Using Single Sign-On with Windows Identity Foundation in MVC .NET

modified

Introduction

It’s common for growing companies to offer multiple web sites and services. Initially, each site may have its own login system, or at the very least, certainly its own login page. As their user base grows, they may eventually find the need to consolidate their network of sites under a common login page and authentication framework. Examples of this include Google (Gmail, Drive, Store, FeedBurner), Microsoft (Outlook, Live, Bing), Yahoo (Mail, Finance, Answers), as well as many others. Unauthenticated users, accessing any of the associated web sites, are redirected to a common login page. The login page usually resides on a completely separate web service and domain, such as login.yahoo.com, login.live.com, or accounts.google.com. In the world of C# ASP .NET, this process is called federated authentication, and can be implemented using the Windows Identity Foundation library.

The Windows Identity Foundation (WIF) comes built into the .NET 4.5 framework. This makes it easy to get started, without the need for installing additional libraries. In this tutorial, we’ll walk through the steps of implementing federated authentication with single sign-on. We’ll create a C# MVC4 ASP .NET web site and a common authentication web site identity provider (also called a Secure Token Service or STS). The identity provider web site will provide the login form, using forms authentication, and redirect back to the calling web site.

At the end of this tutorial, we’ll have a web site using single sign-on via the Windows Identity Foundation that redirects users to login via a common authentication provider. Additional sites may be scaled in, using the same identity provider web site as needed.

Getting Started

We’ll start by creating our identity provider web site (STS). This web site will display the login form and allow a user to enter their username/password. After signing in, the user will be redirected back to the calling web site, as a fully authenticated user.

To get started, we’re going to need an SSL certificate for the identity provider. Specifically, we need the public key. We’ll host the public key as a metadata file on our identity provider web site (the relying party web sites will point to this metadata url in their web.config files). Luckily, we can generate a test cert fairly easily.

Creating an Identity Provider Certificate

First things first. We begin the process by creating an initial SSL certificate that will be used by the identity provider. A test certificate can be created in a variety of ways, such as by using IIS to generate a test cert or by using a tool, such as Pluralsight’s SelfCert creator.

For this example, we’ll use the SelfCert tool and create a test certificate. When creating the certificate, specify the value for “X.500 distinguished name” to be: “cn=customsts.dev”. Choose to save the cert in the location “LocalMachine” under the store “My”. This will ensure the certificate is accessible by the identity provider, when running under the LocalSystem application pool (we’ll get to that in a bit).

Single Sign-on SelfCert tool to create a test certificate

If you get an access violation error while trying to store the certificate, you can simply save the certificate to your desktop and then double-click it to import and store it. Again, be sure to select “Local Machine” as the storage location.

Extracting the Public Key from the Certificate

With the certificate created and stored, next we need to grab the public key. To do this, open a command prompt and type “mmc.exe”. Then click File->Add/Remove Snap-in. Select Certificates and click Add. Select “Computer account” and click Next, then click Finish.

Navigate to Certificates/Personal/Certificates and you should see “customsts.dev” located in the list along the right-side. Double-click the certificate to open the properties. Click the “Details” tab and then click “Copy to File” to export the certificate.

In the export wizard, select “No, do not export the private key”, since we’re only interested in the public key. Then choose “Base-64 encoded X.509 (.CER)” as the export type. Save the exported file to your desktop and open it in Notepad. We’ll need the contents of this key for our identity provider web site’s metadata file.

Creating the Identity Provider Web Site

Hooray, we’ve got a public key. Although that was quite a bit of work, we’re now ready for the next step. It’s time to create the STS web site that will host our login form.

Creating the Federation Metadata File

We’ll begin by creating a new MVC4 C# ASP .NET web site named “SingleSignOn”. In the web project, create a new folder called “Federation”. Inside the folder, add a new XML file called “metadata.xml”. For the contents of this file, paste in the following code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<!--?xml version="1.0" encoding="utf-8"?-->
<entitydescriptor entityid="http://customsts.dev/" id="_70a250d5-e3e1-494a-a392-7ed1736f3180" xmlns="urn:oasis:names:tc:SAML:2.0:metadata">
<roledescriptor protocolsupportenumeration="http://docs.oasis-open.org/wsfed/federation/200706" xmlns:fed="http://docs.oasis-open.org/wsfed/federation/200706" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="fed:SecurityTokenServiceType">
<keydescriptor use="signing">
<keyinfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<x509data>
<x509certificate>
!!! PASTE THE CONTENTS OF YOUR EXPORTED PUBLIC KEY HERE !!!
!!! Everything between -----BEGIN CERTIFICATE----- and -----END CERTIFICATE----- !!!
</x509certificate>
</x509data>
</keyinfo>
</keydescriptor>
<contactperson contacttype="administrative">
<givenname>Your Name</givenname>
</contactperson>
<fed:claimtypesoffered>
<auth:claimtype optional="true" uri="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name" xmlns:auth="http://docs.oasis-open.org/wsfed/authorization/200706">
<auth:displayname>Name</auth:displayname>
<auth:description>The name of the subject.</auth:description>
</auth:claimtype>
<auth:claimtype optional="true" uri="http://schemas.microsoft.com/ws/2008/06/identity/claims/role" xmlns:auth="http://docs.oasis-open.org/wsfed/authorization/200706">
<auth:displayname>Role</auth:displayname>
<auth:description>The role of the subject.</auth:description>
</auth:claimtype>
</fed:claimtypesoffered>
<fed:securitytokenserviceendpoint>
<endpointreference xmlns="http://www.w3.org/2005/08/addressing">
<address>http://customsts.dev/</address>
</endpointreference>
</fed:securitytokenserviceendpoint>
<fed:passiverequestorendpoint>
<endpointreference xmlns="http://www.w3.org/2005/08/addressing">
<address>http://customsts.dev/</address>
</endpointreference>
</fed:passiverequestorendpoint>
</roledescriptor>
</entitydescriptor>

Note the placeholder for your public key string in the above xml. You should copy and paste the public key string from your exported certificate file into the appropriate space above. Be sure not to include the “Begin Certificate” and “End Certificate” tags. Also, take note not to accidentally copy the first xml descriptor line twice (a common mistake when copying/pasting xml).

Configuring the Identity Provider to Support Windows Identity Foundation

Next, we need to add the appropriate settings to our web.config file to enable Windows Identity Foundation. We’ll start by adding the following appSettings keys:

1
2
3
<add key="IssuerName" value="http://customsts.dev/" />
<add key="SigningCertificateName" value="CN=customsts.dev" />
<add key="EncryptionCertificate" value="" />

Next, set the Federation folder available for anonymous users. All users will need to be able to read this file in order to login with single sign on. We’ll deny access to all other pages on the STS identity provider web site, as follows:

1
2
3
4
5
6
7
<location path="Federation">
<system.web>
<authorization>
<allow users="*">
</allow></authorization>
</system.web>
</location>

Next, since our single sign-on common web site will use forms authentication, we need to enable it as follows:

1
2
3
4
5
6
<authentication mode="Forms">
<forms loginurl="/login" slidingexpiration="true" timeout="2880">
</forms></authentication>
<authorization>
<deny users="?"></deny>
</authorization>

Adding the Identity Provider Login Controller

We’re now ready to start coding the identity provider’s web controllers. First, add a reference to your project for System.IdentityModel and System.IdentityModel.Services.

Next, create a new LoginController, which will serve as the login form on the web site.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
[HttpGet]
public ActionResult Index(string returnUrl)
{
ViewBag.ReturnUrl = returnUrl;
return View();
}
[HttpPost]
public ActionResult Index(LoginModel loginModel, string returnUrl)
{
if (ModelState.IsValid)
{
if (loginModel.Username == "user" && loginModel.Password == "password")
{
FormsAuthentication.SetAuthCookie(loginModel.Username, true);
return Redirect(returnUrl);
}
else
{
ModelState.AddModelError("", "The username or password provided is incorrect.");
}
}
ViewBag.ReturnUrl = returnUrl;
return View(loginModel);
}

In the above code, we have a simple hard-coded authentication check. You will probably want to extend this to authenticate against a database or web service. Note, we set the FormsAuthentication cookie, which sets up the MVC ASP .NET User object. After authentication has completed, we redirect to the returnUrl. In this case, it will actually be our Windows Identity Foundation redirect url, where our controller will set the necessary federation properties.

We’re using the following simple LoginModel for the login form:

1
2
3
4
5
6
7
public class LoginModel
{
[Required]
public string Username { get; set; }
[Required, DataType(DataType.Password)]
public string Password { get; set; }
}

With the login controller defined, we can create the associated login view, using the LoginModel as its backing class.

After authenticating and logging in, our Login controller method redirects to the returnUrl. Since we’re using Windows Identity Foundation, the returnUrl will be our root url and will include several QueryString parameters that indicate the type of WIF action that is being performed. In this case, the action will be “wsignin1.0”. Therefore, we’ll now begin creating the HomeController to process this message.

Adding the Identity Provider Home Controller

As was seen in the Login Controller, after authenticating, the user is redirected to the root page on the identity provider web site. It is here that federated authentication will take place via the Windows Identity Foundation framework.

WIF will provide several querystring parameters in the url, indicating the type of action being performed. The common ones include: wsignin1.0 and wsignout1.0. We can process these actions in our HomeController with the following code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public const string Action = "wa";
public const string SignIn = "wsignin1.0";
public const string SignOut = "wsignout1.0";
public ActionResult Index()
{
if (User.Identity.IsAuthenticated)
{
var action = Request.QueryString[Action];
if (action == SignIn)
{
var formData = ProcessSignIn(Request.Url, (ClaimsPrincipal)User);
return new ContentResult() { Content = formData, ContentType = "text/html" };
}
else if (action == SignOut)
{
ProcessSignOut(Request.Url, (ClaimsPrincipal)User, (HttpResponse)HttpContext.Items["HttpResponse"]);
}
}
return View();
}
private static string ProcessSignIn(Uri url, ClaimsPrincipal user)
{
var requestMessage = (SignInRequestMessage)WSFederationMessage.CreateFromUri(url);
var signingCredentials = new X509SigningCredentials(CustomSecurityTokenService.GetCertificate(ConfigurationManager.AppSettings["SigningCertificateName"]));
// Cache?
var config = new SecurityTokenServiceConfiguration(ConfigurationManager.AppSettings["IssuerName"], signingCredentials);
var sts = new CustomSecurityTokenService(config);
var responseMessage = FederatedPassiveSecurityTokenServiceOperations.ProcessSignInRequest(requestMessage, user, sts);
return responseMessage.WriteFormPost();
}
private static void ProcessSignOut(Uri url, ClaimsPrincipal user, HttpResponse response)
{
var requestMessage = (SignOutRequestMessage)WSFederationMessage.CreateFromUri(url);
FederatedPassiveSecurityTokenServiceOperations.ProcessSignOutRequest(requestMessage, user, requestMessage.Reply, response);
}

The first item to note is our definition of actions, via the querystring parameters, for signing in and signing out. For sign-in, a formdata string is created, which gets returned back to the browser as a form post, thus completing the sign-in process. This technique is described in detail in a great blog post at Building a simple custom STS using VS2012 & ASP.NET MVC.

The HomeController uses a utility security class, CustomSecurityTokenService.cs (provided via the author in the above link), to handle processing the sign-in message. This class inherits from SecurityTokenService and provides the required methods for processing the sign-in request.

Note, since the MVC controller requires access to the HttpResponse object, we’ll need to store a copy of this earlier on in the MVC ASP .NET request chain. We can do this within BeginRequest in Global.asax.cs as shown below. The HttpContext.Items object is persisted for a single HTTP request, which is just long enough for us to access it in the controller.

1
2
3
4
protected void Application_BeginRequest(Object sender, EventArgs e)
{
HttpContext.Current.Items.Add("HttpResponse", Response);
}

Windows Identity Foundation Redirect Flow

If you monitor the network requests, upon accessing a protected page on the relying party web site, you’ll see the following series of request created from our Login and Home controllers:

  1. User accesses a protected page on the relying party web site:
1
2
3
4
5
http://localhost/user
302 redirect
http://customsts.dev/?wa=wsignin1.0&wtrealm...
/login?ReturnUrl=
200 OK

The final “200 OK” request is where our identity provider web site login form displays, allowing the user to enter their credentials on the central login form. After the user enters their details and submits the form, the following series of requests are issued:

  1. User enters their credentials on the central login page and clicks Submit:
1
2
3
4
5
6
7
POST http://customsts.dev/login?returnUrl=
302 redirect
http://customsts.dev/?wa=wsignin1.0&wtrealm...
200 OK
POST http://localhost:667 <= This is where the form data is posted back
302 redirect
http://localhost

The first POST checks the username and password and sets the FormsAuthentication cookie. It then issues a redirect to the returnUrl. Since we’re using Windows Identity Foundation, the returnUrl leads us to the root page on our identity provider web site. Since the user has been properly authenticated via forms authentication at this point, the root page processes the federated authentication and issues a form POST back to the relying web site, with the WIF STS security token (trust:RequestSecurityTokenResponseCollection) as part of the form post data.

The process flow is completed on the relying party site, upon processing the STS token and authenticating the user on the initiating relying party site.

Creating a Relying Party Web Site

With our central single sign-on web site defined using C# MVC ASP .NET Windows Identity Foundation and STS, we can now create our relying party web site (a web site that authenticates using the single sign-on identity provider site). To begin, create a new C# MVC4 ASP .NET web site. We’ll need to configure the web.config to use Windows Identity Foundation WIF. This can be done with the Visual Studio 2012 Identity and Access add-on or it can be configured manually, as shown below.

Above the appSettings section in the web.config, add the following configSection declarations:

1
2
3
4
<configsections>
<section name="system.identityModel" type="System.IdentityModel.Configuration.SystemIdentityModelSection, System.IdentityModel, Version=4.0.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089" />
<section name="system.identityModel.services" type="System.IdentityModel.Services.Configuration.SystemIdentityModelServicesSection, System.IdentityModel.Services, Version=4.0.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089" />
</configsections>

Add the following appSettings:

1
2
3
<add key="ida:FederationMetadataLocation" value="http://customsts.dev/federation/metadata.xml" />
<add key="ida:Issuer" value="http://customsts.dev/" />
<add key="ida:ProviderSelection" value="productionSTS" />

Note, the first appSetting key provides the public url to our metadata XML file, created in the first step of this tutorial.

Next, if any pages on this web site will be public (not require a logged-in user), you can define them as being available to anonymous users with a location block, as follows:

1
2
3
4
5
6
7
<location path="public">
<system.web>
<authorization>
<allow users="*">
</allow></authorization>
</system.web>
</location>

The above code sets the path /public available for all (including non-authenticated) users. For all other pages, we’ll define them as protected, requiring a login. We’ll also disable the default authentication, allowing WIF to take over handling authentication. Add the following code under the system.web section:

1
2
3
4
<authorization>
<deny users="?" />
</authorization>
<authentication mode="None" />

Alternatively, if you want all pages to be public, except for certain folders, you can use the following:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<location path="user">
<system.web>
<authorization>
<deny users="?">
</deny></authorization>
</system.web>
</location>
<system.web>
...
<authorization>
<allow users="*" />
</authorization>
<authentication mode="None" />
...
</system.web>

Under the section system.webServer, we’ll need to define the WIF authentication modules, as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<modules>
<remove name="FormsAuthentication" />
<add name="WSFederationAuthenticationModule" precondition="managedHandler" type="System.IdentityModel.Services.WSFederationAuthenticationModule, System.IdentityModel.Services, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
<add name="SessionAuthenticationModule" precondition="managedHandler" type="System.IdentityModel.Services.SessionAuthenticationModule, System.IdentityModel.Services, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
</modules><system.identitymodel>
<identityconfiguration>
<audienceuris>
<add value="http://localhost:667/" />
</audienceuris>
<issuernameregistry type="System.IdentityModel.Tokens.ConfigurationBasedIssuerNameRegistry, System.IdentityModel, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<trustedissuers>
<add name="http://customsts.dev/" thumbprint="123456789588c015487c239d8e37ee7adabcdefa" />
<!-- SET TO THE THUMBPRINT FROM YOUR CERTIFICATE -->
</trustedissuers>
</issuernameregistry>
<certificatevalidation certificatevalidationmode="None">
</certificatevalidation></identityconfiguration>
</system.identitymodel>
<system.identitymodel.services>
<federationconfiguration>
<cookiehandler requiressl="false">
<wsfederation issuer="http://customsts.dev/" passiveredirectenabled="true" realm="http://localhost:667/" reply="http://localhost:667/" requirehttps="false">
</wsfederation></cookiehandler></federationconfiguration>
</system.identitymodel.services>

Note, you can find the thumbprint string for the above configuration by opening mmc.exe (as shown in the steps in the first part of this tutorial), opening the properties of the identity model certificate, click the Details tab, and scroll down to the Thumbprint field. You’ll probably have to type this value in manually (mmc doesn’t appear to allow copy/paste), so type it carefully.

Also, add a reference to your project for System.IdentityModel and System.IdentityModel.Services.

Creating the Relying Party Controllers

We’ll create a plain HomeController to serve as the root landing web site page. This page will contain a link pointing to /login, allowing the user to manually sign in.

1
2
3
4
5
6
7
public class HomeController : Controller
{
public ActionResult Index()
{
return View();
}
}

For the login page, we’ll create a LoginController. We won’t actually need a view for this page, since we’ll be immediately redirecting to the identity provider web site. However, MVC will throw an error if a view does not exist. A default view can be added for the LoginController. Inside the login controller Index method we’ll add a call to the Windows Identity Foundation redirect call. This manually redirects the user to the central STS single sign-on login web page. Alternatively, you could simply direct the user to a protected page, such as /user, in which case the web.confing “location” permissions will automatically direct him to the identity provider login page, as well. You can test both scenarios by clicking the Sign In link and by navigating directly to /user as an unauthenticated user. Both actions will lead to the central federated authentication login page.

1
2
3
4
5
6
7
8
public class LoginController : Controller
{
public ActionResult Index()
{
FederatedAuthentication.WSFederationAuthenticationModule.RedirectToIdentityProvider("customsts.dev", "http://localhost:667/user", true);
return View();
}
}

Our third controller will be for protected (logged-in) user content. The UserController will have a main Index page, where we’ll simply display @User.Identity.Name for the authenticated logged-in user, and a Logout method. The Logout method will delete the cookie on the relying party web site and on the identity provider web site. It does this through another flow of redirects to the identity provider, in order to properly delete both cookies and clear out the authentication ticket.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class UserController : Controller
{
public ActionResult Index()
{
return View();
}
public ActionResult Logout()
{
// Load Identity Configuration
FederationConfiguration config = FederatedAuthentication.FederationConfiguration;
// Sign out of WIF.
WSFederationAuthenticationModule.FederatedSignOut(new Uri(ConfigurationManager.AppSettings["ida:Issuer"]), new Uri(config.WsFederationConfiguration.Realm));
return View();
}
}

Setting Up IIS to Host the STS Identity Provider

With the web sites developed, we’ll want to try testing out the login process. First, you should set the proper security on the web folders by ensuring the user IIS_IUSRS has read access to both the identity provider STS web site and the relying party web site.

Next, open IIS and create a new Application Pool. Set the “Identity” property on the application pool to run under “LocalSystem”, so it can access the certificate (that we stored in the first step of this tutorial).

Create a new web site under “Sites”, named “customsts.dev”. Point it to the folder for your identity provider web site project. For the bindings, use the host name “customsts.dev” and choose port 80. Since we’re using a custom host name, we’ll also have to modify the system32/drivers/etc/hosts file to include a setting for 127.0.0.1 customsts.dev

Note, be sure to set the application pool for the customsts.dev web site to use the LocalSystem identity, as mentioned above.

Next, create a new web site under “Sites”, named “relyingparty1”. Point it to the associated folder in your web project. Set this site to run on port 667 (or one of your choosing - if you change the port, adjust the settings in the web site code accordingly).

Testing Single Sign-On

At last, we can open a web browser and navigate to http://localhost:667. You should see the public home page with a link to Sign In. Clicking the Sign In link directs the user to the central single sign-on STS site. Alternatively, navigating directly to the user-protected page http://localhost:667/user also directs the user to the central STS site, but does so via the authorization web.config setting.

Upon logging in, you’ll be redirected back to the relying party web site. This time, you’ll be automatically logged in and have access to the /user page. We’ve successfully completed single sign-on with forms authentication in C# MVC ASP .NET with Windows Identity Foundation and STS.

Troubleshooting Tips

While configuring single sign-on, you may come across a variety of issues or errors, many related to security and permissions. I’ve included some of the more common ones that I’ve experienced while configuring my own setup for enterprise single sign-on.

Error: Keyset does not exist.

Solution: The AppPool user needs permissions to the certificate store (the certificate must be added with a private key, then select the certificate, right-click certificate, select All Tasks, Manage Private Keys) or simply set the user to LocalSystem. Keep in mind the security implications of using LocalSystem for the certificate user, although this usually resolves this error.

Error: The issuer of the security token was not recognized by the IssuerNameRegistry. To accept security tokens from this issuer, configure the IssuerNameRegistry to return a valid name for this issuer.

Solution: Select web.config in Visual Studio, right-click web.config and choose “Open With”, select “Binary Editor”. Look for hidden control characters in the thumbprint. Verify thumbprint is all upper-case characters. Verify name (next to thumbprint) matches certificate issue name “CN=sub.domain.com” in web.config for single sign-on site and client relying party site. It is recommended to re-type the thumbprint manually into the web.config, using all capital letters (do not copy and paste the thumbprint from the certificate properties dialog into the web.config, as this will copy hidden control characters into the web.config thumbprint and cause the above mentioned error).

Error: Key not valid for use in specified state.

Solution: This error is due to the federated cookie being assigned to ‘/‘ on the same domain (probably localhost, during development). This error should go away on different hosted domains. You may also wish to try editing the settings for the client relying party site in IIS, by clicking Advanced Settings and setting LoadUserProfile=true.

Error: No certificate was found for subject Name CN=login.domain.com.

Solution: Use a fully distinguised CN name, including ALL parts: “CN=login.domain.com, O=one, W=two, T=three, Z=four” (no spaces, except one space after each comma).

There are many bits and pieces of code samples for implementing Windows Identity Foundation and single sign-in in C# MVC ASP .NET with forms authentication. Many are customized instances for specific scenarios. For more details, check out the following helpful pages:

Using Claims-Identity with SimpleMembership in ASP.NET MVC
How To: Build Claims-Aware ASP.NET MVC Web Application Using WIF
Federated Identity with Multiple Partners
Building a simple custom STS using VS2012 & ASP.NET MVC

Download @ GitHub

Source code for the project is available on GitHub.

About the Author

This article was written by Kory Becker, software developer and architect, skilled in a range of technologies, including web application development, machine learning, artificial intelligence, and data science.

Share