DarksideCookie

Come to the dark side...we have cookies!

Building a simple custom STS using VS2012 & ASP.NET MVC

In my previous post, I walked through how “easily” one can take advantage of claims based authentication in ASP.NET. In that post, I switched out the good old forms authentication stuff for the new FedAuth stuff. In this post, I want to take it a step further and actually federate my security, but instead of just using the Windows Azure ACS’s built in identity providers, I want to build a very simple one of my own.

A lot of the solution is based on the STS project that we could get by using VS2010 and the WIF SDK. However, this project was a Web Site project using Web Forms, and I really wanted a MVC version for different reasons.

If you are fine with using VS2010 and the WIF SDK, adding a custom STS is really easy. Just create a new web project, right-click the project and choose “Add STS Reference…” and then, walking through the wizard, there will be a step that offers you to select an STS. In this step, you choose “Create a new STS project…”, which will generate a custom STS project that you can modify to your needs. Unfortunately, that option isn’t available in VS2012. Using the “Identity and Access” add-on, you are only allowed to connect to an existing STS, the ACS or a local test STS, not an STS project.

So, the task is to create a custom STS based on the stuff from the WIF SDK, but updated to run MVC and VS 2012. The task however, is NOT to create and ADVANCED and configurable STS that will replace things like ADFS and Thinktecture’s Identity Server. It will be a very simple STS that can be extended and modified to pretty much whatever one might need. You could for example combine it with my previous post and build and STS based on the ASP.NET providers… Neither is it the goal to create a very well architected application. The goal is to create an STS that works as a proof of concept. It will have a bunch of coupling and hard-coded values that really should be refactored out to config and so on, but the main goal was to show the general idea…

Ok, after all of that disclaimer stuff, it is time to get started!

I start with a new empty MVC 4 project, and even if “empty project” actually means almost empty nowadays, I still remove the NuGet package for WebApi (as well as all related packages). Once that is gone, there is still the matter of removing a line of code in Global.asax.cs to get the whole thing to build… But once that is done, I can start looking at the actual implementation…

Unfortunately, there is actually one more step before I can do that. I need to prepare a certificate to use for signing the tokens. In this case, I am just quickly generating a new self-signed cert using a tool that can be found here. It is a GUI-based way of creating a self-signed cert which removes the need to get down and dirty with the command-line and learning the 20 parameters needed to create a cert.

I save the cert to a pfx and put it somewhere where I can find it (on the desktop of course). Next I install the cert by double clicking it, choosing to install it using LocalMachine and letting the installation tool decide where to put it. Once it has been installed, I need to get hold of the public key, which is not that hard. Using mmc.exe and the Certificate snap-in I can export the cert as a Base-64 encoded .CER file. Once I have the CER file, I can open it in Notepad and get the public key, which I will need in a minute…

Now that I have a cert for signing the tokens, it is time to add the metadata Xml needed for relying parties to interact with the STS. I fudged this by stealing the metadata file from the WIF SDK implementation and switching out some values

In my version, it looks like this

<?xml version="1.0" encoding="utf-8"?>
<EntityDescriptor ID="_70a250d5-e3e1-494a-a392-7ed1736f3180" entityID="http://customsts.dev/" xmlns="urn:oasis:names:tc:SAML:2.0:metadata">
<RoleDescriptor xsi:type="fed:SecurityTokenServiceType" protocolSupportEnumeration="http://docs.oasis-open.org/wsfed/federation/200706"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:fed="http://docs.oasis-open.org/wsfed/federation/200706">
<KeyDescriptor use="signing">
<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<X509Data>
<X509Certificate>[MY MASSIVELY LONG KEY]</X509Certificate>
</X509Data>
</KeyInfo>
</KeyDescriptor>
<ContactPerson contactType="administrative">
<GivenName>Chris Klug</GivenName>
</ContactPerson>
<fed:ClaimTypesOffered>
<auth:ClaimType Uri="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name" Optional="true" 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 Uri="http://schemas.microsoft.com/ws/2008/06/identity/claims/role" Optional="true" 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>

The parts that I have modified are:
The “entityID” attribute on the EntityDescriptor element.
The “GivenName” for the “ContactPerson”
The “Address” fields for the “EndpointReference”s
And finally the content of the “X509Certificate” element. This value is the key from the CER file put on one line (everything between “-----BEGIN CERTIFICATE-----“ and “-----END CERTIFICATE-----“).

It is possible to generate new FederationMetadata.xml files using different tools, or by hand if you for some reason know the exact Xml required by heart. You can even dynamically generate the Xml using code if you want to. If that is your thing, I suggest looking around the web or possibly here. (I haven’t tried what is said in that post, but it came up while Googling…)

Ok, now that I have the metadata that describes how the STS works and the certificate, I guess it is time to implement the STS functionality…

The first thing I create is an AccountController with a single Login() action. The Login() action takes a string parameter called “returnUrl” which will be used when redirecting from the login. I add the returnUrl to the ViewBag and return a view.

public ActionResult Login(string returnUrl)
{
ViewBag.ReturnUrl = returnUrl;
return View();
}

The view in itself is ridiculously simple. It gives the user a form to be used for logging in, including a username textbox and a password textbox. The form is set to post back to a Login() action, including the returnUrl as a querystring

@using (Html.BeginForm(new { returnUrl = ViewBag.ReturnUrl }))
{
...
}

The target action uses the posted data to authenticate the user and log in the user using FormsAuthentication. It then redirects the user back to the returnUrl.

[HttpPost]

public ActionResult Login(LoginModel model, string returnUrl)
{
if (ModelState.IsValid && model.UserName.Equals("chris", StringComparison.OrdinalIgnoreCase) && model.Password.Equals("password"))
{
FormsAuthentication.SetAuthCookie(model.UserName, model.RememberMe);
return Redirect(returnUrl);
}

ViewBag.ReturnUrl = returnUrl;
ModelState.AddModelError("", "The user name or password provided is incorrect.");
return View(model);
}

Yes, FormsAuthentication! I know I said I was going to do FedAuth stuff, and still I am using FormsAuth. However, this is only locally on the STS. This will be used to create the FedAuth token later on. The cool thing is that as long as the FormsAuth cookie is in place and valid, the user will automatically be logged in when sent to the STS. Basically enabling single sign on (SSO).

That is actually it for the authentication part. As you can see, I am using hard-coded values, which sucks…but it is a demo! Simple to switch out for real stuff though…

To get the forms stuff going, I need to add some config to the web.config. It looks like this

<system.web>
<authentication mode="Forms">
<forms loginUrl="~/Account/Login" timeout="2880" />
</authentication>
<authorization>
<deny users="?" />
</authorization>
...
</system.web>

So…so far the flow is the following: The user browse to the relying party. The Relying party redirects the user to the STS. The STS redirects the user to the log in page. The user logs in and gets redirected back to the root page.

So, what happens at the root page? Well, that is where the token is generated and sent back to the relying party.

I guess it is time to add a HomeController to the solution. The HomeController’s Index() action checks if the user is authenticated, which he/she always should be as the site is using FormsAuthentication denying unauthorized users access.

If the user is logged in, it checks the querystring for a parameter called “wa”. This is added by the federation module when redirecting the user to the STS. If the value of the “wa” parameter is “wsignin1.0”, the user wants to sign in, which would be the standard scenario. If the “wa” parameter is there and set to “wsignin1.0”, I create an HTML form on the fly, and send that back to the user.

public const string Action = "wa";
public const string SignIn = "wsignin1.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" };
}
}
return View();
}

Ok, so the flow is now extended with another redirect. In this case, a form is created including the authentication token. This form is then automatically posted back to the relying party. Flow complete!

But as the curious person you are, you have probably realized that I have not done any federated auth stuff at all. All i have done is call a method called ProcessSignIn() which returns an HTML form in the form of a string. And no, ProcessSignIn() isn’t some neat built in thing… So let’s look at how it creates the form!

The first thing I do in the ProcessSignIn() method, is to create a SignInRequestMessage instance, using WSFederationMessage.CreateFromUri(). However, to be able to use these classes, I first add a reference to System.IdentityModel and System.IdentityModel.Services.

Besides the SignInRequestMessage, I also need signing credentials. In my case I create these by creating a new X509SigningCredentials, passing in the cert I created earlier, and put in the cert store. This requires a little bit of code, but I will not cover that. If you want to know how to get a cert from the store, I suggest Googling it, or downloading my sample code at the end…

Ok, now I have a SignInRequestMessage and a X509SigningCredentials object. On top of that, I need a SecurityTokenServiceConfiguration. You can either inherit this, and do some funky stuff like they do in the WIF SDK, or you can do just create an instance of it, passing it the issuer name of the STS and the signing credentials, which is what I do.

In the WIF code, they cache this configuration, which makes me assume that it is a little heavy to create. So in a high-load scenario, I suggest doing that. But being a demo, keeping it simple is the way to go…

The last thing I need (yes, I need even more stuff) is an instance of a class that inherits from SecurityTokenService. In my case, I have created one called CustomSecurityTokenService, which I will get back to in a minute. So I create one of those, passing it the configuration.

Ok, I finally have all the little bits and pieces I need to create my response message. This is created by using the static ProcessSignInRequest() method on the FederatedPassiveSecurityTokenServiceOperations class. Once I have a response message, I can use it to get the HTML form.

private static string ProcessSignIn(Uri url, ClaimsPrincipal user)
{
var requestMessage = (SignInRequestMessage)WSFederationMessage.CreateFromUri(url);
var signingCredentials = new X509SigningCredentials(GetCertificate(ConfigurationManager.AppSettings["SigningCertificateName"]));
var config = new SecurityTokenServiceConfiguration(ConfigurationManager.AppSettings["IssuerName"], signingCredentials);
var sts = new CustomSecurityTokenService(config);
var responseMessage = FederatedPassiveSecurityTokenServiceOperations.ProcessSignInRequest(requestMessage, user, sts);
return responseMessage.WriteFormPost();
}

Ok, there you have it! A whole bunch of standard classes from the framework created and put together in a useful matter. The only thing that I haven’t been covered is the CustomSecurityTokenService, which is responsible for some parts of the generation of the token. More specifically it is the class responsible for setting up a the “Scope” and the identity to add to the token.

When inheriting from SecurityTokenService, you need to do 3 things. You need to pass a SecurityTokenServiceConfiguration to the base class, and implement the GetScope() and GetOutputClaimsIdentity() methods.

The interesting parts are the 2 methods… Let’s start with the GetScope() method, in which you are responsible for creating a new Scope instance and configure it for potential token encryption.

For the sake of simplicity, I will ignore 2 things in this post. The first one being able to limit what relying parties are allowed to use the STS, which can be done by looking at the AppliesTo property of the RequestSecurityToken instance. And the second one being encryption. The sample code includes both, but since I will neither limit the use of the STS, nor encrypt the token by default, I will just skip over that… But it should be done in the GetScope() method…

So all I need to do is to create a new Scope() instance, passing it the Url of the relying party as well as the credentials used for signing the token.

Next, I set the ReplyToAddress of the Scope, which defines where the user is redirected when the form is posted. I pull this Url from the RequestSecurityToken’s ReplyTo property, which is set when configuring the relying party.

protected override Scope GetScope(ClaimsPrincipal principal, RequestSecurityToken request)
{
var scope = new Scope(request.AppliesTo.Uri.OriginalString, SecurityTokenServiceConfiguration.SigningCredentials);

scope.ReplyToAddress = request.ReplyTo;

return scope;
}

The second method, the GetOutputClaimsIdentity(), is just as simple. All that is need here, is to create a new ClaimsIdentity and add the required claims. In this case, I only set the Name and NameIdentifier claims. Like this

protected override ClaimsIdentity GetOutputClaimsIdentity(ClaimsPrincipal principal, RequestSecurityToken request, Scope scope)
{
var claims = new[]
{
new Claim(System.IdentityModel.Claims.ClaimTypes.Name, principal.Identity.Name),
new Claim(System.IdentityModel.Claims.ClaimTypes.NameIdentifier, principal.Identity.Name),
};

var identity = new ClaimsIdentity(claims);

return identity;
}

Ok, that is all there is to it! A custom STS done and dusted!

However, I guess the question is if it works…? :)

To verify this, I set up the STS in my IIS using a hostheader with the name “customsts.dev”, which I have added in my hosts file. Next, I switch the identity used by the app pool to LocalSystem to give it access to the certificate store. Once I have a site in the IIS and the identity set, it is time to create a new relying party, which is just a glorified new ASP.NET web application.

In the new “relying party” web application, I use the “Identity and Access” add-on in VS to add a “reference” to my STS. During the configuration of the STS, I pointed it to “http://customsts.dev/FederationMetadata/2007-06/FederationMetadata.xml”, disabled “require ssl”. This will re-write the web.config file and add all the required federation stuff. All but one little thing… To get the ReplyTo of the request set properly, I manually had to add the “reply” attribute to the wsFederation element like this

...
<wsFederation passiveRedirectEnabled="true" issuer="http://customsts.dev/" realm="http://localhost:49285/"
reply="http://localhost:49285/" requireHttps="false" />
...

That’s it! The add-on configures the rest for us. So browsing to the new web, I get redirected to the STS, where I can log in and get redirected back to the web with a token that authenticates me.

The code for this is available here: DarksideCookie.AspNet.FedAuth.CustomSTS.zip (1.37 mb)

Cheers!

Posted: Apr 09 2013, 09:43 by ZeroKoll | Comments (4) |
  • Currently 0/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5
Manage post: :)

Comments (4) -

Stefan Karlsson Sweden said:

Hi Chris,
Ty for a good tutorial. I have tried to follow along but i can't get it to work properly.

I have described my problem att stackoverflow:

http://stackoverflow.com/questions/17384889

Ty.

# June 29 2013, 23:38

ZeroKoll Sweden said:

Hello Stefan!
There are some issues when working with certificates once in a while. And by issues, I guess I means "snags", not actual bugs or something... If the cert is installed under your user, then the site will not be able to access it, unless you have set up your application pool to use your identity. Try putting it under your computer account instead, and switch the app pool to use Local System. That should work... If not, let me know!
// Chris
(I hope I am not off target...StackOverflow isn't working at the moment, and I am in Australia with crappy network at a motel...)

# July 06 2013, 12:59

Stefan Karlsson Sweden said:

Hi again Chris!
Hope you had a nice stay in Australia!

Well i figured that out after a long time of testing...
I know find the cert, well.. i guess i do Smile

The problem i get now is that i can construct X509SigningCredentials without a private key.

Full error message:
ID2057: Cannot construct a X509SigningCredentials instance for a certificate without the private key.
Parameternamn: token

Is the problem that i havent created a password when i created the certificate? And if it is, were do i place the password?

Ty once again Chris!

# August 21 2013, 19:15

Stefan Karlsson Sweden said:

I solved the issue.
The issue was the certificates private key, i created a new cert using makecert.exe.

# August 22 2013, 19:54

Pingbacks and trackbacks (3)+

Add comment




  Country flag
biuquote
Loading