Customize the Default #WIF ASP.NET #STS to Support Multiple #SharePoint Web Applications
Thanks to Claims-based identity in SharePoint 2010 and 2013, we have the ability to develop our own trusted login providers. These providers take the form of a Security Token Service (STS), an ASP.NET web site that packages and signs SAML tokens containing claims. SharePoint is then configured to trust this STS. In this blog post, I walk through the steps required to develop a custom STS using the Windows Identity Foundation (WIF) SDK 4.0 that supports LinkedIn as an identity provider for SharePoint. However, there is one significant shortcoming with the approach I use in that post: it can only be used for a single SharePoint web application, intranet.contoso.com. This is due to two factors:
- The realm used by the SPTrustedIdentityTokenIssuer is configured as the URL of SharePoint’s STS within that web application (e.g., intranet.contoso.com/_trust).
- The default behavior of the WIF STS web site is to forward tokens for authenticated users to the specified realm URL. From there, SharePoint’s STS will forward the logged in user back to the page he or she was trying to access.
If you tried to use this trusted identity provider on another web application (such as hr.contoso.com or finance.contoso.com), you would end up disappointed as your users would be forwarded back to the intranet.contoso.com web application no matter which site they were actually trying to access!
In this blog post, I will make some slight modifications to the out-of-the-box WIF STS web site so that:
- The same trusted identity provider may be used across multiple different web applications, each with different host header URLs.
- The SPTrustedIdentityTokenIssuer does not need to be updated and can leverage the same realm value no matter how many web applications make use of it.
- The WIF STS web site I build and customize does not need to be updated and can leverage the same realm value no matter how many web applications make use of it.
What is a realm?
The default behavior of the WIF STS web site is to forward an authenticated user back to the URL specified as the realm. According to MSDN, the realm value is “used by the STS to identify the relying party instance and to select the corresponding token issuance policy and encryption certificate.” The realm is specified as a URI (which can actually be a URL or a URN), but in many walkthroughs (mine included), we specify a value for the realm that is tied to a specific host header URL (e.g., intranet.contoso.com/_trust).
Decoupling the realm from a specific host header URL
In an environment where only one web application needs to use a trusted identity provider, it may make sense to associate the realm with the URL of that web application. But in an environment where multiple claims web applications in a SharePoint farm need to use that same trusted identity provider, a different approach must be taken. As Steve Peschka describes in this post, we can use a URN (e.g., urn:linkedin:sts) instead of a URL to work around this limitation. In the case of ADFS v2 (disclaimer: I have not worked with ADFS v2), you can define mappings between these URNs and their associated web applications as Steve describes in the blog post linked above.
Updating the default WIF STS web site
If I change my SPTrustedIdentityTokenIssuer to use the generic realm urn:linkedin:sts, but make no changes to my STS web site, what will happen? As it turns out, I will be redirected to this URN, which of course is not the URL I want my logged in users to visit. Hilarity ensues:
As you can see, the STS redirects the logged in user to urn:linkedin:sts, which of course produces a 404 error.
The key to having the STS redirect logged in users to the proper URL is in the CustomSecurityTokenService class, which contains a function GetScope() that sets the ReplyToAddress of the SecurityTokenService.Scope object. The default STS web site implementation uses the following code:
// Set the ReplyTo address for the WS-Federation passive protocol (wreply). This is the address to which responses will be directed. // In this template, we have chosen to set this to the AppliesToAddress. scope.ReplyToAddress = scope.AppliesToAddress;
The AppliesToAddress value is determined from the wtrealm parameter specified as the “OAuth Accept Redirect URL” in our LinkedIn application configuration. Remember that we had previously set this value to intranet.contoso.com/_trust (which would work as a URL), but have since updated it to urn:linkedin:sts.
How can I fix this?
The way you choose to work around this behavior depends on your requirements. If you need to use different token encrypting certificates based on the relying party (RP) application, or if you want to potentially execute any custom business logic specific to some (but not all) of your RP applications, you may choose to update the code in the GetScope() function to look more like the following:
if (scope.AppliesToAddress == "urn:linkedin:sts") { scope.EncryptingCredentials = new X509EncryptingCredentials(CertificateUtil.GetCertificate(StoreName.My, StoreLocation.LocalMachine, "EncryptingCert1")); scope.ReplyToAddress = "http://intranet.contoso.com/_trust"; // More things specific to the intranet web application and the urn:linkedin:sts realm... } else if (scope.AppliesToAddress == "urn:someother:sts") { scope.EncryptingCredentials = new X509EncryptingCredentials(CertificateUtil.GetCertificate(StoreName.My, StoreLocation.LocalMachine, "EncryptingCert2")); scope.ReplyToAddress = "http://extranet.contoso.com/_trust"; // More things specific to the extranet web application and the urn:someother:sts realm... }
This approach carries the following “baggage” (which may be completely necessary based on your requirements):
- The STS has to be recompiled and redeployed anytime a new web application needs to be supported.
- It still cannot be reused across any host-header named web applications.
But what if I don’t have any special requirements? What if I just want my STS to redirect authenticated users back to whichever web application referred them, even new web applications that I don’t know about yet? We can do this! The way I chose to accomplish this is as follows:
- In Login.aspx.cs of my STS web site (which is the first page a user will hit prior to being authenticated), I can grab the host header from the UrlReferrer property of HttpContext.Current.Request and store it in a session variable with /_trust appended. This will serve as the realm value I want my STS to use and will contain the value of the referring SharePoint web application.
- In Default.aspx.cs of my STS web site, see if the session variable I created above exists. If it does, pass it in to the constructor for my CustomSecurityTokenService class.
- In App_Code/CustomSecurityTokenService.cs, store the updated realm value in a member variable. If the STS was called with a realm value of urn:linkedin:sts (some minimal validation of the RP), redirect the user to the appropriate realm URL in the GetScope() function.
Here are the relevant code snippets required to accomplish this:
Login.aspx.cs:
Uri referrer = HttpContext.Current.Request.UrlReferrer; if (referrer != null) { string defaultRealm = referrer.ToString(); // From SharePoint, this value will be: defaultRealm = defaultRealm.Substring(0, defaultRealm.IndexOf("_login")).TrimEnd('/') + "/_trust/"; Session["defaultRealm"] = defaultRealm; }
Default.aspx.cs:
// Process signin request. SignInRequestMessage requestMessage = (SignInRequestMessage)WSFederationMessage.CreateFromUri( Request.Url ); string defaultRealm = ""; if (Session["defaultRealm"] != null) { defaultRealm = Session["defaultRealm"] as string; } if ( User != null && User.Identity != null && User.Identity.IsAuthenticated ) { SecurityTokenService sts = new CustomSecurityTokenService( CustomSecurityTokenServiceConfiguration.Current, defaultRealm ); SignInResponseMessage responseMessage = FederatedPassiveSecurityTokenServiceOperations.ProcessSignInRequest( requestMessage, User, sts ); FederatedPassiveSecurityTokenServiceOperations.ProcessSignInResponse( responseMessage, Response ); }
App_Code/CustomSecurityTokenService.cs:
private string defaultRealm = string.Empty; public CustomSecurityTokenService( SecurityTokenServiceConfiguration configuration, string realm = "" ) : base( configuration ) { defaultRealm = realm; } protected override Scope GetScope( IClaimsPrincipal principal, RequestSecurityToken request ) { ValidateAppliesTo( request.AppliesTo ); Scope scope = new Scope( request.AppliesTo.Uri.OriginalString, SecurityTokenServiceConfiguration.SigningCredentials ); string encryptingCertificateName = WebConfigurationManager.AppSettings[ "EncryptingCertificateName" ]; if ( !string.IsNullOrEmpty( encryptingCertificateName ) ) { // Important note on setting the encrypting credentials. // In a production deployment, you would need to select a certificate that is specific to the RP that is requesting the token. // You can examine the 'request' to obtain information to determine the certificate to use. scope.EncryptingCredentials = new X509EncryptingCredentials( CertificateUtil.GetCertificate( StoreName.My, StoreLocation.LocalMachine, encryptingCertificateName ) ); } else { // If there is no encryption certificate specified, the STS will not perform encryption. // This will succeed for tokens that are created without keys (BearerTokens) or asymmetric keys. scope.TokenEncryptionRequired = false; } // Set the ReplyTo address for the WS-Federation passive protocol (wreply). This is the address to which responses will be directed. // In this template, we have chosen to set this to the AppliesToAddress. if (scope.AppliesToAddress == "urn:linkedin:sts") { scope.ReplyToAddress = defaultRealm; } else { scope.ReplyToAddress = scope.AppliesToAddress; } return scope; }
Putting it all together
After deploying this updated STS, any new SharePoint web application I create that leverages the LinkedIn trusted identity provider will redirect users back to the appropriate URL after they are authenticated, regardless of whether the new web applications use a different host header, port, or both. To prove this, I created a new web application on port 39668 and set a “Full Read” user policy for all LinkedIn users. Upon accessing the site, I choose to sign in using my LinkedIn identity provider:
After supplying my credentials, LinkedIn will redirect me back to my STS. My STS will then determine where to redirect me based on the code above, and voila, I will be taken to my new SharePoint site, authenticated and signed in!