WIF SSO and Forms Authentication in ASP.NET
One of the projects on which we are working is a long-lived ASP.NET Web Forms system that is customized for a specific client. It is hosted by another company on a server which is external to the client’s environment and it does not have an access to the client’s internal network. The system is built by using the Form Authentication mechanism to authenticate and authorize the users. The list of users and their hashed passwords is stored into the database and the login functionality works in a classic manner – the credentials provided by the user on the login page are validated against the list of users in the database. If the provided credentials are valid then a new Forms Authentication session is established by calling the standard method FormsAuthentication.SetAuthCookie().
Recently, we had to extend that authentication mechanism by adding a single sign-on (SSO) capability which allows the client to integrate the ASP.NET web application with their internal Active Directory (AD) infrastructure. The requirement was to allow some internal employees to access the ASP.NET web application through SSO, but also keep the exiting database login functionality for the rest of the users who are external and they do not have internal AD accounts.
The ASP.NET web application is hosted on an external server and it does not have a direct access to the secured AD infrastructure. After doing some research, we found that in order to connect the external ASP.NET web application to the internal AD environment we can use a middle service called Security Token Service (STS).
The STS is an identity provider responsible for authenticating users and issuing security tokens used by claims-aware applications. Claims-aware applications rely on a trusted external service to perform the authentication of the users and the exchanged claims contain information about who is the user, what is his name, email, department, etc.
A good example of a STS is Active Directory Federation Services (AD FS) which is a platform built by Microsoft. However, in our case the client had already done some integration with Okta – a third-party platform for connecting people, applications and devices. We had just to integrate our ASP.NET website with Okta’s federated services.
I would like to add a note regarding the STS and the development environment. Usually, STS is complex software which requires a lot of effort for configuration and getting running. So, we found pretty helpful that Microsoft has created a template in Visual Studio called “ASP.NET Security Token Service Web Site”. It could be used to build a simple web site that is running locally as STS while developing your claims-aware applications.
So, our main goal was to make our existing ASP.NET Web Forms application to become claims-aware and integrate it with the STS (Okta in our case). Microsoft already had built a relatively easy way to build claims-aware applications. The Windows Identity Foundation (WIF) has been fully integrated into the .NET Framework and it makes building such applications a trivial task. The problem is that in order to enable the WIF’s modules and make the web application claims-aware the website’s existing authentication mechanism should be turned off. More specifically, the web.config should be updated so that the standard authentication mechanism is disabled as follows:
<authentication mode="None">
This was not an option in our case because we still had to support the existing database login functionality for external users. In other words, we had to keep the ASP.NET application using Forms Authentication:
<authentication mode="Forms">
Then we decided to create a separate ASP.NET application which is claims-aware and which relies on the external STS to authenticate the users. The idea is to integrate that claims-aware ASP.NET application with the main ASP.NET website by sharing the same Forms Authentication sessions. In that way the main ASP.NET website would support the both authentication types and the flow could be described as follows:
The user tries to access the website, he is not authenticated yet and there are two options as follows:
a) The user provides his credentials on the standard login page. The username and the password are validated against the database and if everything is good a new Forms Authentication session is established and the user is allowed to continue with the main website.
b) The user decides to access the application with SSO. He is redirected to the claims-aware sub-project. The claims-aware application automatically redirects the user to the STS (Okta in our case). The user is authenticated by the STS either by providing his credentials manually or detecting them automatically via NTLM, depending on if the user is connected to the internal network. If everything is good then the STS redirects the user back to the claims-aware application by sending claims containing information about the particular user (e.g. username, email address, first name, last name, etc.). The claims-aware application checks the claims and if the authenticated user has rights to access the main ASP.NET application then a new Forms Authentication session is established and the user is redirected to the main ASP.NET application. It is important to notice that the Forms Authentication session is shared between the claims-aware sub-project and the main ASP.NET website.
Following are the steps and the technical details that we did in order to get the SSO part working:
1) Create a new small ASP.NET claims-aware application
We added a new small ASP.NET web application to our solution. It has only one page without actual UI. It is responsible just to handle the SSO requests.
2) Configure the small claims-aware ASP.NET web application and connect it to the STS via federated services
This involves only web.config settings. We had to set the following settings in web.config:
<configuration> <configSections> <section name="microsoft.identityModel" type="Microsoft.IdentityModel.Configuration.MicrosoftIdentityModelSection, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/> </configSections> ... <system.web> <authorization> <deny users="?"/> </authorization> ... <authentication mode="None"> </authentication> ... <httpModules> <add name="SessionAuthenticationModule" type="Microsoft.IdentityModel.Web.SessionAuthenticationModule, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/> <add name="WSFederationAuthenticationModule" type="Microsoft.IdentityModel.Web.WSFederationAuthenticationModule, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/> </httpModules> ... <system.webServer> <modules> <add name="SessionAuthentication" type="Microsoft.IdentityModel.Web.SessionAuthenticationModule, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" preCondition="managedHandler" /> <add name="WSFederationAuthenticationModule" type="Microsoft.IdentityModel.Web.WSFederationAuthenticationModule, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" preCondition="managedHandler" /> </modules> <validation validateIntegratedModeConfiguration="false"/> </system.webServer> ... <microsoft.identityModel> <service> <certificateValidation certificateValidationMode="None"/> <audienceUris> <add value="http://localhost/SSOApp/"/> </audienceUris> <issuerNameRegistry type="Microsoft.IdentityModel.Tokens.ConfigurationBasedIssuerNameRegistry, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"> <trustedIssuers> <add thumbprint="XXXXYYYYZZZZ" name="PassiveSigninSTS"/> </trustedIssuers> </issuerNameRegistry> <federatedAuthentication> <wsFederation passiveRedirectEnabled="true" issuer="http://stsservername/" realm="http://localhost/SSOApp /" requireHttps="false" /> <cookieHandler requireSsl="false"/> </federatedAuthentication> </service> </microsoft.identityModel>
When working with IIS and the sample STS website, you can get the thumbprint from the test certificate’s hash in IIS. Be careful with copy-pasting that thumbprint! It is very likely that you will see the “ID4175: The issuer of the security token was not recognized by the IssuerNameRegistry.” error. The cause could be an extra character at the beginning of the copied value which is invisible. A possible workaround would be to type the thumbprint manually.
3) Read claims for authenticated users
When the user is authenticated by the STS then it is redirected to the claims-aware sub-project. The request includes the various claims (e.g. username, email address, etc.) for the user. We can read all that information in the code and use it appropriately. To read the claims we use the following sample C# code:
ClaimsPrincipal claimsPrincipal = Thread.CurrentPrincipal as ClaimsPrincipal; foreach (ClaimsIdentity claimIdentity in claimsPrincipal.Identities) { msg += "Identity: " + claimIdentity.Name + "<br/>"; foreach (Claim claim in claimIdentity.Claims) { msg += "Type=" + claim.ClaimType + "<br/>" + "Value=" + claim.Value + "<br/>" + "ValueType=" + claim.ValueType + "<br/>" + "Subject.Name=" + claim.Subject.Name + "<br/>" + "Issuer=" + claim.Issuer + "<br/><br/>"; } }
It is up to you to decide how to handle that information. For example, in our project we check if the username coming from the claims, which is the AD username, exists into the website’s database. If a matching username if found then we establish a new sessions, otherwise the user is not allowed to access the system.
4) Establish a new Forms Authentication sessions shared between the two ASP.NET applications
When the user is authenticated by the SSO claims-aware web application then we setup a new Forms Authentication that is valid for the main ASP.NET website too.
It is possible to share the same Forms Authentication session between two ASP.NET applications when the following points are true:
a) The applications are running on the same web domain.
b) The applications use the same machine keys.
In our case the main ASP.NET website and the claims-aware sub-project are running on the same server and same domain. So, we had just to set the same values in web.config files for the machine keys. For example:
<machineKey validationKey="XXXYYYZZZ" decryptionKey="AAABBBCCC" validation="SHA1" />
Also, the Forms Authentication cookies must be the same. That is why the <forms/> element must be configured with the same values into the two web.config files. Basically, we are re-using the values from the main ASP.NET website and we are putting them into the claims-aware project’s web.config as follows:
<authentication mode="None"> <forms name=".ASPXFORMSAUTH" protection="All" path="/" timeout="60" requireSSL="false" /> </authentication>
Please notice that we are keeping <authentication mode=”None”> because of this is a WIF claims-aware project, but we are putting the <forms/> nested element in order to setup the Forms Authentication settings. Basically, the application is claims-aware and it does not work with Forms Authentication. But we are still able to configure the parameters of the cookie and we are able to establish such a cookie from the code by calling the following code even though Forms Authentication is not enabled:
FormsAuthentication.SetAuthCookie(username, false);
It will establish a new Forms Authentication session that will be valid into the main ASP.NET website as well and we are ready to redirect the user into the actual application.
5) Redirect the user into the main application
The Forms Authentication session is established and we are just redirecting the user to the main ASP.NET web application.
Now we have an ASP.NET website that supports two login types – standard Forms Authentication and SSO based on claims! Thank you for reading this!