Claims-based Identity and Access Control Guide RTM!
March 5th, 2010
I found myself posting more on twitter than my blog. However this deserved a post.
The RTM of the guide is finally out there in PDF version.
- Book content online on MSDN.
- Book PDF download
- Final samples download
- Discuss at Codeplex
Looking at my name in the cover of a book together with such a group of experts is really a significant milestone in my career. I want to specially thanks Eugenio for trusting me and inviting me to participate in this project. Hope you find the content useful. If you have any questions or you want to discuss about claims, identity, federation towards your next project feel free to mail me at matias at southworks dot net.
Now heading towards the second book: Cloud Guidance! Stay tuned…
Claims based Authentication & Authorization: The Guide
August 15th, 2009
Eugenio announced yesterday the kickoff of a new guide from patterns & practices in which I’m collaborating: Claims based Authentication & Authorization Guide.
This is not a new topic as Eugenio suggests in his blog, but it’s getting more and more attention because:
- Technology is more mature, hence it’s easier to implement claim-based identity
- Enterprises are failing to control the amount of different identity repositories, leading to higher provisioning/deprovisioning costs, security problems, etc.
- End users want simpler user experiences and less passwords
- The cloud makes all these even more challenging
We started with this project a couple of weeks ago planning the content. The approach we decided to use was heavily driven by scenarios (aka zero bulls**t). We used the visual metaphor of a tube map with scenarios being the stations separated in two main lines:
- The blue one, the Enterprise track approaches the federated identity problem from the point of view of a company with many applications that wants to implement SSO and Federation. The main stations are SSO (within the enterprise), Federation (with partners), SOAP Web Services (and flow of identity across services), SSO with a third party cloud app and some variations like: what if the company decides to host an application on the cloud (namely Windows Azure); or what if the company needs to integrate with an application that talks SAML protocol (i.e. Salesforce, Google Apps)
- The yellow one, ISV track on the other hand tackle the problem from the perspective of an ISV that wants to offer an application as a service (think about Salesforce or Dynamics CRM Online as the canonical examples). In this track we start by explaining how to implement federated identity for a cloud application. Then we show how to automate federation to on board new customers. We also show things like exposing a REST API and how that plays with claims; how to integrate with LiveID (or OpenID) for small customers that don’t have an Identity Provider in place; and we end up explaining how to do auditing/billing with claims.
I’m very proud and excited about being part of such a great team including: Dominick Baier, Vittorio Bertocci, Keith Brown, David Hill and Eugenio Pace. I’m sure that something great will come up from this team, the board of reviewers and the community that will help to prioritize and keep the focus!
OpenID – WS-Fed Protocol Transition STS
July 14th, 2009
I will go straight to the point in this post. This is a possible architecture if you want to allow OpenID authentication in a claims-aware WS-Federation-compatible web application. In this architecture there are three actors:
- the web application (aka the relying party)
- the OpenID provider (myopenid, Google, Yahoo, etc.)
- the “protocol broker” STS that “translates” WS-Fed to OpenID and viceversa
These are the interactions that happen at login time:
NOTE: the diagram shows the interactions in a conceptual fashion. In reality all these arrows are HTTP requests/responses that are originated in the user browser.
- The user browse the app
- The Geneva Framework WSFederationAuthenticationModule detects the user is anonoymous and is trying to access a protected page. So it will create a WS-Fed SignIn Request against an STS
- This STS, built with Geneva Framework, will provide a login page that will do the handshake against an OpenID Provider. To do this we use DotNetOpenAuth. In fact this STS will be an OpenID Relying Party.
- The user will provide its OpenID identifier and the STS will issue an authentication request against the OpenId Provider
- The OpenID Provider will ask for a password and will return an authentication response to the STS
- The STS will grab the claims issued by the OpenID provider and will put them in the ClaimsIdentity, generating a SAML token with OpenID claims.
- The STS will return a WS-Fed SignIn Response with the SAML token. The WSFederationAuthenticationModule will grab the token, validates it and generate a principal.
- The user can now access a restricted page because it’s authenticated. But also we have profile information (if the user filled his profile in the OpenID provider)
So essentially what we’ve done is a Protocol Transition STS (don’t know if such term exists), that will transform WS-Fed to OpenID and viceversa.
| Aside This is powerful because we can now plug this STS with a Geneva Server and keep all of our applications WS-Fed compatible. Geneva Server can be used as an R-STS that all the applications will trust. This Geneva Server might be configured with different Identity Providers, one of them could be the “OpenID STS” that we’ve just described. |
Here are some screenshots working with myopenid.com and Google OpenId provider.
Notice that we are getting back some profile information (like email, full name, etc.). We are translating that profile info into SAML attributes that will be issued by the OpenID STS. The following code shows what we are doing on the OpenID STS login page. We translate the OpenID claims and store them in session to get them later from the STS.
protected void OpenID_OnLoggedIn(object sender, OpenIdEventArgs e) { Dictionary<string, string> claims = GetClaims(e.Response); HttpContext.Current.Session.Add(”OpenIDClaims”, claims); } private Dictionary<string, string> GetClaims(IAuthenticationResponse response) { Dictionary<string, string> claims = new Dictionary<string, string>(); claims.Add(System.IdentityModel.Claims.ClaimTypes.Name, response.FriendlyIdentifierForDisplay); var claimsResponse = response.GetExtension<ClaimsResponse>(); if (claimsResponse == null) return claims; if (claimsResponse.BirthDate.HasValue) claims.Add(System.IdentityModel.Claims.ClaimTypes.DateOfBirth, claimsResponse.BirthDate.Value.ToString(”o”)); if (claimsResponse.Country != null) claims.Add(System.IdentityModel.Claims.ClaimTypes.Country, claimsResponse.Country); if (claimsResponse.Culture != null) claims.Add(”http://openid-custom/identity/claims/culture”, claimsResponse.Culture.ToString()); if (claimsResponse.Email != null) claims.Add(System.IdentityModel.Claims.ClaimTypes.Email, claimsResponse.Email); if (claimsResponse.FullName != null) claims.Add(”http://openid-custom/identity/claims/fullname”, claimsResponse.FullName); if (claimsResponse.Gender.HasValue) claims.Add(System.IdentityModel.Claims.ClaimTypes.Gender, claimsResponse.Gender.Value == Gender.Female ? “Female” : “Male”); if (claimsResponse.Language != null) claims.Add(”http://openid-custom/identity/claims/language”, claimsResponse.Language); if (claimsResponse.Nickname != null) claims.Add(”http://openid-custom/identity/claims/nickname”, claimsResponse.Nickname); if (claimsResponse.PostalCode != null) claims.Add(System.IdentityModel.Claims.ClaimTypes.PostalCode, claimsResponse.PostalCode); if (claimsResponse.PostalCode != null) claims.Add(System.IdentityModel.Claims.ClaimTypes.Locality, claimsResponse.TimeZone); return claims; }
This is the code in the STS:
protected override IClaimsIdentity GetOutputClaimsIdentity( IClaimsPrincipal principal, RequestSecurityToken request, Scope scope ) { ClaimsIdentity outputIdentity = new ClaimsIdentity(); if ( null == principal ) { throw new InvalidRequestException( “The caller’s principal is null.” ); } var openIdClaims = HttpContext.Current.Session["OpenIDClaims"] as Dictionary<string, string>; foreach (var openIdClaim in openIdClaims) { outputIdentity.Claims.Add(new Claim(openIdClaim.Key, openIdClaim.Value)); } return outputIdentity; }
Logging in with Google OpenID provider
Since Google does not provide profile info, we get a hash as a login name. We could use OAuth and fetch profile attributes from Google (like email, name, etc.) in the STS and fill more claims. But to do that you need to host the site publicly and register it at Google, it does not work in localhost.
Summary
In this post we showed how you can use Geneva Framework on your claims-aware applications and authenticate against OpenID which is a different protocol. What I really like about Geneva Framework is that it allows you to transition any authentication scheme with WS-Fed because it plays well with the ASP.NET pipeline. So it’s basically an adapter between *any* existing authentication investment and WS-Federation and SAML token. This reminds me of a blog post Vittorio wrote some time ago Enhance your ASP.NET Membership-based website by adding Identity Provider capabilities (hint: read the last paragraph of his post).
Here is the code for the STS and a sample relying party created with the Claims-Aware Website template from Geneva Framework.
Identity thoughts #2: Level 2 Authorization
June 17th, 2009
In my last post I talked about an identity roadmap and how we are helping companies to achieve Level 1: Externalizing Authentication. In this first level, we only care about checking the credentials of a user in a Security Token Service and issue a token with a couple of claims. That token will be enough to prove access to the application.
Reaching the Level 1 will make a great difference to the IT department of a given company. By having a central login place they will be able to answer to questions like “when someone logged in to a certain application?”, “which applications someone used in this timeframe?”, etc. In terms of governance, having a single way to implement login will allow the architecture department (if any) to decrease security threats because there is a single well thought piece of infrastructure to perform user authentication across all apps. As a side effect it will also reduce costs of development and maintenance.
Level 2 talks about the authorization process. The authorization decision happens near the application or the service because it knows about the resource (each application has a different domain model).
Geneva Server – Level 1 Authentication
- SSO & Federation – unified login experience and federation with partners
- Centralized Claim Mapping management
- Externalize authentication
- Near the identity provider
- Questions it will answer
- When a subject logged in to an app?
- What claims the subject presented to the app?
Policy Server - Level 2 Authorization
- Policy Enforcement
- Centralized Policy Rules management
- Externalize authorization
- Near the application
- Questions it will answer
- What permissions did the subject requested?
- What permissions where denied?
The following figure shows a very high level architecture of the components and its interactions
Resources
[XACML] http://www.oasis-open.org/committees/xacml/
[Geneva Server] http://connect.microsoft.com/content/content.aspx?ContentID=10106&SiteID=642
[SAML] http://www.oasis-open.org/specs/#samlv2.0
The following table shows an analogy of identity concepts between a single application and a federated application.
The single app has its own identity silo and the federated app relies on an STS (like Geneva Server). I find this analogy useful to explain how things differ from the non-federated non-claim-based world.
Geneva Framework: Identity Development Training Kit
May 15th, 2009
During the last couple of months I’ve been helping the Microsoft DPE team (namely Vittorio and Donovan) building the Identity Development Training Kit. It’s been great to work with such knowledgeable guys like them and with one of the best frameworks I’ve ever developed with: Microsoft Geneva Framework. ![]()
The training kit covers a lot of interesting scenarios related to claim-based identity. Here is the shortcut list (if you want a full explanation of each one, read Vittorio’s post)
- Lab: Web Sites and Identity:
- Exercise 1: Enabling claims based access for an ASP.NET Web Application by generating a local STS
- Exercise 2: Customizing the Credentials Accepted by a Local STS
- Exercise 3: Accepting Tokens from a Geneva Server STS
- Exercise 4: Accepting Tokens from Live ID
- Exercise 5: Accepting Tokens from .NET Access Control Service
- Exercise 6: Invoking a WCF Service on the Backend via Delegated Access
- Lab: Enhancing an ASP.NET Membership Provider Website with Identity Provider Capabilities
- Lab: Web Services and Identity
- Exercise 1: Using Geneva Framework for Handling Authentication and Authorization in a WCF Service
- Exercise 2: Accepting Tokens from a Geneva Server STS
- Exercise 3: Accepting Tokens from .NET Access Control Service
- Exercise 4: Invoking a WCF Service on the Backend via Delegated Access
We made sure that all of the exercises followed the best practices of developing with Geneva Framework. Building this training kit was a big effort and I would like to mention the great team that helped creating this: Ariel “lutz” Neisen, Jonathan “passive” Cisneros, Ezequiel “checklist” Sculli and Sebastian “pattern” Iacomuzzi
I invite you to take a look at the training kit and open your mind with the new possibilities the Geneva Framework brings into the table.
UPDATE: the code has been updated to work with WIF RTM. Thanks Nico!
Providing the federation metadata for your STS will be very useful when a relying party want to establish a trust relationship with your STS. For instance, the Geneva Framework provides a FedUtil.exe tool that allows you to point to this metadata file and configure the relying party changing the microsoft.identityModel section (read more about the metadata format here: http://www.oasis-open.org/committees/download.php/30005/ws-federation-1.2-spec-ed-09.doc)
The metadata is signed with the STS private key, which make sense because you don’t want someone else publishing a metadata file and claiming that it’s your STS metadata. That means that you will need some code in order to generate that signature based on the metadata content.
Well I have good news for you. Microsoft Geneva Framework provides a couple of useful classes (like MetadataSerializer) to generate the metadata.
Disclaimer: this code generates a simple version of federation metadata for an IP passive STS (it does not include WS-Trust endpoints for active profile for instance).
var stsUri = new Uri(“https://login.mysts.com/FederationPassive”); string destFolder = @”d:\Temp\”; string signingCertificateSubjectName = “CN=localhost“; var claimsOffered = new DisplayClaim[] { CreateDisplayClaim(”http://schemas.xmlsoap.org/claims/Group”, false, “Group”, string.Empty), CreateDisplayClaim(“http://schemas.xmlsoap.org/claim/Issuer”, false, “Issuer”, string.Empty), CreateDisplayClaim(“http://schemas.xmlsoap.org/claim/Email”, false, “Email”, string.Empty), CreateDisplayClaim(“http://schemas.xmlsoap.org/claim/FirstName”, false, “FirstName”, string.Empty), CreateDisplayClaim(“http://schemas.xmlsoap.org/claim/LastName”, false, “LastName”, string.Empty), CreateDisplayClaim(“http://schemas.xmlsoap.org/claim/CostCenter”, false, “CostCenter”, string.Empty), CreateDisplayClaim(“http://schemas.xmlsoap.org/claim/Phone”, false, “Phone”, string.Empty) }; CreatePassiveStsMetadata(stsUri, signingCertificateSubjectName, claimsOffered, destFolder);
The code above shows the usage for a sample STS. Download the code from here
Multi tenant federation with Geneva Framework and Microsoft .NET Services Access Control
April 23rd, 2009
A typical scenario for an ISV that wants to create the "next application in the cloud" will be how to support identity federation with their customers (tenants). A common requirement I’ve heard is:
"I want to enable single sign on and allow enterprises that have their own STS to integrate with us. For companies that don’t have any identity infrastructure in place we want to allow them to login with an ubiquous credential like Windows LiveID. How do we do that without spending three months with a security guru?"
A possible answer is use Microsoft .NET Services Access Control. They enable that scenario in a very straightforward fashion. The following diagram shows a possible architecture that might fulfill the customer requirements. In this picture Southworks is an enterprise that has its own STS and Contoso doesn’t, hence they use Windows LiveID for their users. The good thing about this is that in the middle we have ACS acting as the "normalizer". It will receive tokens from LiveID and Southworks IP STS and will transform them to something Fabrikam knows (Roles, Actions, etc.).
If you are like me, you might be wondering how this all works. Here are the gory details of all the HTTP interactions of a WS-Federation passive profile "dance":
- A user opening a browser
- If he is at Contoso he browses to www.fabrikam-cloudapp.com/contoso (or contoso.fabrikam-cloudapp.com)
- If he is at Southworks he browses to www.fabrikam-cloudapp.com/southworks (or southworks.fabrikam-cloudapp.com)
- At the web site there is an Http Module or ActionFilter in ASP.NET MVC that will read the tenant alias from the route.
- The module will construct the SignInRequestMessage federation message and will redirect to ACS (https://{solution-name}.accesscontrol.windows.net/passivests/{federation-endpoint}).
- If it’s Southworks tenant we have to use the https://{solution-name}.accesscontrol.windows.net/passivests/Federation.aspx endpoint of ACS.
- If it’s Contoso we have to use https://{solution-name}.accesscontrol.windows.net/passivests/LiveFederation.aspx in the endpoint of ACS.
Note: these urls are not in any ACS documentation for now - The homeRealm (whr) parameter will tell ACS which IP STS to use (Contoso = login.live.com, Southworks = login.southworks.net which is a url Southworks provided at provisioning probably).
- Finally, the realm parameter is fabrikam-cloudapp application. This will have to match with the scope you create on ACS (more on that below).
Note: Look at the code at the end of the post to see how homeRealm and realm is used. - ACS will redirect the user to either LiveID or the company IP STS depending on the whr parameter
- The user will login with LiveID cred or with other mechanisms if it’s a Geneva Server (user/pwd, kerberos, certs, cardspace, etc.)
- A token will be issued and will be POSTed to ACS federation endpoint
- [Contoso] In the case of LiveID you don’t have to do anything because ACS setup all the federation for us. LiveID will issue a token with one claim: the WLID email.
- [Southworks] In the case of the company IP STS you will have to configure ACS as a relying party and supply its certificate to encrypt the token in your IP STS. In ACS you will have to create an Issuer and upload the company IP STS certificate. The company STS will issue the name claim and maybe groups or other claims.
- ACS will read the token and apply the claim transformation (more on that below)
- ACS will POST the token to www.fabrikam-cloudapp.com
- Geneva Fx on the website will read the token, generate the ClaimsPrincipal and store it in a cookie for later usage.
- Fabrikam can now authorize access to certain pages by reading the principal! either with IsInRole or for granular checks you could use Actions or a ClaimsAuthorizationManager
This is how ACS is configured for :
A better way to look at the table above is the following diagram:
Here we are using the Geneva Fx manually inside an ASP.NET MVC action filter. We use it manually because the multi tenancy nature that we want to implement does not allow to use the fixed values from the wsFederation configuration section.
1: namespace FabrikamCloudApp.Web.Identity
2: {
3: using System;
4: using System.Globalization;
5: …
6:
7: [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = false)]
8: public sealed class WSFederationAuthenticationFilterAttribute : ActionFilterAttribute, IExceptionFilter
9: {
10: public override void OnActionExecuting(ActionExecutingContext filterContext)
11: {
12: var principal = Thread.CurrentPrincipal;
13:
14: if (!principal.Identity.IsAuthenticated)
15: {
16: string realm;
17: string homeRealm;
18: string acsEndpoint;
19: SignInRequestMessage message = null;
20:
21: // retrieve tenant from url (http://…/{tenant})
22: var tenant = (string)filterContext.RouteData.Values["tenant"];
23:
24: // lookup in some repository if the tenant setup a federation with its own sts
25: if (IsLiveIDFederation(tenant))
26: {
27: acsEndpoint = "LiveFederation.aspx";
28: homeRealm = "http://login.live.com";
29: }
30: else
31: {
32: acsEndpoint = "Federation.aspx";
33: homeRealm = GetIdentityProviderUrl(tenant);
34: }
35:
36: realm = "http://www.fabrikam-cloudapp.com/";
37: string issuer = "https://{solution-name}.accesscontrol.windows.net/passivests/{federation-endpoint}"
38: .Replace("{solution-name}", Configuration.FabrikamSolutionName)
39: .Replace("{federation-endpoint}", endpoint);
40:
41: message = new SignInRequestMessage(new Uri(issuer), realm);
42: message.Parameters.Add("whr", homeRealm);
43:
44: // redirect to ACS
45: filterContext.Result = new RedirectResult(message.WriteQueryString());
46: }
47: else
48: {
49: // we are back on our site, and we’ve got our token transformed to ClaimsPrincipal
50: var claimsIdentity = principal.Identity as IClaimsIdentity;
51:
52: var tenantClaim = claimsIdentity.Claims.FirstOrDefault(c => c.ClaimType == Configuration.TenantClaimType);
53: if (tenantClaim == null)
54: {
55: throw new HttpException(401, "Access is denied");
56: }
57:
58: var fam = FederatedAuthentication.WSFederationAuthenticationModule;
59: if (fam.CanReadSignInResponse(HttpContext.Current.Request, true))
60: {
61: SecurityToken token = fam.GetSecurityToken(HttpContext.Current.Request);
62: SessionSecurityToken sessionToken = new SessionSecurityToken(principal as IClaimsPrincipal, token);
63: fam.SetPrincipalAndWriteSessionToken(sessionToken, true);
64:
65: filterContext.Result = new RedirectToRouteResult(
66: "DefaultRoute",
67: new RouteValueDictionary(new { tenant = tenantClaim.Value }));
68: }
69: }
70: }
71:
72: }
73: }
And just put this attribute on your entry point Controller:
1: public class HomeController
2: {
3: [WsFederationAuthenticationFilter]
4: public void Index()
5: {
6: }
7: }
I can tell my customer "sure, we can implement this. It’s only 73 lines of code and some configuration here and there"
Happy Federation!
IssueTracker Azure Edition - a Cloud Application
February 13th, 2009
Couple of weeks ago Ryan Dunn announced Azure Issue Tracker. From this post:
"This sample application is a simple issue tracking service and website that pulls together a couple of the Azure services: SQL Data Services and .NET Access Control Service."![]()
I’ve been working with Ryan and other guys at DPE and Southworks to put together this sample before PDC. With all the back and forth (the .NET services were not working as reliable as they work now) we were not able to pull it through at that time. Well, it’s now live and you can download the source code. Some of its features:
- [Identity] .NET Services Access Control as a relying party and claims transformation STS
- [Identity] Federation against LiveID and claim mapping between email -> tasks. I hinted the implementation in these post.
- [Identity] Claims aware application and service layer (by doing identity delegation with ActAs)
- [Data] Storage on SDS using the flexible schema to extend the data model of the issue
- [General] Multi tenancy at all levels (identity, data, programming model)
- [General] Clean separation of concerns using ASP.NET MVC, Geneva Framework, WCF and WF.
This is the standard edition. The enterprise edition is coming with features related to manageability (Management API, Powershell CmdLets, MMC, SCOM, etc.) and identity federations against third party STS. Stay tuned!
Azure Services Platform - Passive Federation & Access Control #2
November 9th, 2008
In the previous post I introduced a scenario where you can use .NET Services Access Control and Windows LiveID to delegate authentication and authorization. In this post we will go through the different pieces needed in the application to perform authorization checks. First thing will be configure the passive federation using Geneva on the application and later we will create an ASP.NET MVC action filter to perform the access check against the incoming claims.
Note: all the code showed here is using Microsoft Identity Framework "Zermatt" Beta 1. The new Geneva Framework might have some changes.
Configuring passive federation on the website
Configure passive federation on the website is about defining which SAML token version we will accept and the certificate we will use to decrypt the incoming token. The following configuration uses Zermatt Beta 1, so this probably changes on Geneva.
<microsoft.identityModel> <tokenHandlers> <remove type="Microsoft.IdentityModel.Tokens.Saml11.Saml11TokenHandler, Microsoft.IdentityModel, Version=0.4.1.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" /> <add type="Microsoft.IdentityModel.Tokens.Saml11.Saml11TokenHandler, Microsoft.IdentityModel, Version=0.4.1.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"> <samlSecurityTokenRequirement> <allowedAudienceUris> <add value="http://localhost/YourApp/" /> </allowedAudienceUris> </samlSecurityTokenRequirement> </add> </tokenHandlers> <federatedAuthentication enabled="true"> </federatedAuthentication> <serviceCertificate> <certificateReference findValue="01 20 …" storeLocation="LocalMachine" storeName="My" x509FindType="FindByThumbprint" /> </serviceCertificate> </microsoft.identityModel>
When the user click on the sign in button, the link will point to to the .NET Services Access Control passive STS url. The following method uses Geneva to generate this WS-Federation url.
private static string GetFederationUrl(string realm, string issuer, string homeRealm, string returnUrl) { FederatedAuthenticationModule fam = new FederatedAuthenticationModule(); fam.Realm = realm; fam.Issuer = issuer; fam.Reply = returnUrl; SignInRequestMessage signInMsg = fam.CreateSignInRequest(); signInMsg.Parameters.Add("whr", homeRealm); string url = signInMsg.WriteQueryString(); return url; }
The following code and configuration will give you an idea of the url that is being built. Pay attention to this url because a small change might break the whole thing.
string url = GetFederationUrl(ConfigurationManager.AppSettings["AccessControlRealm"], ConfigurationManager.AppSettings["AccessControlIssuer"], ConfigurationManager.AppSettings["AccessControlHomeRealm"], replyTo);
<!– Windows Azure Federation –> <add key="AccessControlRealm" value="http://localhost/YourApp/"/> <!– should match to a scope –> <add key="AccessControlIssuer" value="https://accesscontrol.windows.net/passivests/yoursolution/LiveFederation.aspx"/> <add key="AccessControlDefaultReply" value="http://localhost/YourApp" /> <add key="AccessControlHomeRealm" value="http://login.live.com" />
The AccessControlRealm config is important because it will match the scope on your solution. You will have to configure the scope to encrypt with the public key of your website certificate and create the claim mappings from Windows LiveID to your well known claims. If you don’t have the scope created or configured to output at least one claim you will get a 403 Forbidden on the .NET Services Access Control STS.
Performing access check in the web site
Now that we have everything configured and the token should be coming back to our website, it’s time to do the access check. By using Geneva, the token will be transformed to a Principal object and it will be accessed through the ClaimsPrincipal static class. On the other hand, ASP.NET MVC allow us to plug into the action execution pipeline and get access to the context data like route values. The following code shows an ActionFilterAttribute that will grab the claims from the the Geneva ClaimsPrincipal and will call a strategy class that will perform the access check. If the access check is not successful, the filter will render a NotAuthorized view.
namespace YourApp.Identity { using System; … public class ClaimAuthorizationRouteFilterAttribute : ActionFilterAttribute { public ClaimAuthorizationRouteFilterAttribute(string[] operations) { this.Operations = operations; } public string[] Operations { get; set; } public override void OnActionExecuting(ActionExecutingContext context) { var identity = ClaimsPrincipal.Current.Identity as IClaimsIdentity; var claims = identity.Claims.ToArray(); var routeData = context.RouteData.Values.ToArray(); var strategy = CreateAuthorizationStrategy(); var executionContext = new ExecutionContext() { ClaimsNeeded = Operations, OperationContextData = routeData, }; if (!strategy.IsAuthorizedFor(executionContext, claims)) { context.Result = new ViewResult { ViewName = "NotAuthorized" }; } base.OnActionExecuting(context); } } }
Finally, the following code shows an implemented strategy for a multi tenant application that manage projects.
namespace YourApp.Identity { using System.Linq; using System; public class StandardAuthorizationStrategy : IAuthorizationStrategy { private const string ProjectClaimType = "urn:Project"; private const string TenantClaimType = "urn:Tenant"; private const string OperationClaimType = "urn:Operation"; public bool IsAuthorizedFor(ExecutionContext context, Microsoft.IdentityModel.Claims.Claim[] claims) { bool authorized = true; var tenantClaim = claims.SingleOrDefault(c => c.ClaimType == TenantClaimType); var operationClaims = claims.Where(c => c.ClaimType == OperationClaimType); var projectClaims = claims.Where(c => c.ClaimType == ProjectClaimType); var tenant = context.OperationContextData["Tenant"].ToString(); var project = context.OperationContextData["Project"].ToString(); if (!string.IsNullOrEmpty(tenant)) { authorized &= tenantClaim.Value.Equals("*", StringComparison.OrdinalIgnoreCase) || tenantClaim.Value.Equals(tenant, StringComparison.OrdinalIgnoreCase); } if (!string.IsNullOrEmpty(project)) { authorized &= projectClaims.Where( p => p.Value.Equals("*", StringComparison.OrdinalIgnoreCase)).Count() > 0 || projectClaims.Where( p => p.Value.Equals(project, StringComparison.OrdinalIgnoreCase)).Count() > 0; } if (context.Operations != null) { bool temp = true; foreach (string op in context.ClaimsNeeded) { temp &= operationClaims.Where(o => o.Value.Equals(op, StringComparison.OrdinalIgnoreCase)).Count() > 0 || operationClaims.Where(o => o.Value.Equals("*", StringComparison.OrdinalIgnoreCase)).Count() > 0; } authorized &= temp; } return authorized; } } }
The only thing left is to put an attribute above the action. The following attribute specifies that the New action will be executed if the incoming token contains the following "urn:Operation" claims.
public class ProjectsController : Controller { [ClaimAuthorizationRouteFilter(new string[] { "AddUser", "AddUsersToProject", "CreateProject" })] public ActionResult New() { …. } … }
So if a user browses to: http://yourapp/Contoso/Projecsts/New, the filter will call the strategy that will check:
- if the user contains a tenant claim with the value "Contoso" (taken from the route data)
- if the user contains three operation claims: AddUser, AddUsersToProject and CreateProject
And if a user browses to: http://yourapp/Contoso/Projecsts/some-project/Edit, the filter will call the strategy that will check:
- if the user contains a "tenant" claim with the value "Contoso" (taken from the route data)
- if the user contains the "operation" claims specified in the Edit action
- if the user contains a "project" claim with the value "some-project"
