It happens every time we create a new ASP.Net application we talk about doing something better than using ASP.Net membership but it never happens and before you know it your live using the SqlMembershipProvider database. Now you are trapped because removing it would be a big change and the business requires you to focus on more important features.
BUT that is not true, this post will go through the steps required to separate your authentication from your application whilst maintaining your original membership database giving you a Single Sign-On (SSO) Security Token Service (STS). The additional benefits of this mean that you can use the same STS for all your applications and focus your efforts on writing websites, not having to re write authentication and user management code.
I plan to in a later post show how you can use Azures Access Control Service (ACS) to separate your applications from a singe STS and allow you the flexibility of introducing additional STSs without having to change your applications.
Installing Windows Identity Foundation (WIF)
The first step is to pull out the authentication process from your application and move it to it’s own web application. There are a number of ways to do this but the easiest way is to install the Windows Identity Foundation (WIF) Framework and SDK which will plug-in to Visual Studio. This will provide some UI tools which will make things easier as well as providing a number of assembly libraries which will be used to create and consume the security token. See the download links at the end of the article.
Creating your ASP.Net STS
To start off we need to create a website that will be our STS. The STS takes over the role of authentication so that your website can concentrate on doing what it should do. In this example I am going to create a MVC4 website but if you are not bothered you can get Visual Studio to create you a dummy asp.net project, which we will do later anyway, and just use that.
Create a new ASP.Net MVC4 Internet Web Site project
Create a new Asp.Net MVC4 Internet Website project and add it to your solution, if you select blank website it will not create the AccountController for you and you will have to do this by hand. My examples below assume you are using the Razor view engine but feel free to use the view engine of your choice and adapt the code appropriately.
Change the login post action in the AccountController to do the following:
if (Membership.Provider.ValidateUser(model.UserName, model.Password))
{
FormsAuthentication.SetAuthCookie(model.UserName, model.RememberMe);
return Redirect(returnUrl);
}
Create a Custom Security Token service for your domain
The easiest way to do this is to get Visual Studio to generate the boiler plate code for you and then change it as you see fit. Most of my changes involved separating out the components so that I could do DI via Windsor.
- To generate the code first make sure you comment out the “system.serviceModel” configuration section from your web config otherwise Visual Studio will think it is a WCF project.
- Right click on your website and select “Add STS reference…” as mentioned previously. Enter the Uri to the root of your website including the port number.
- Click “Next” and then click “Yes” if prompted.
- Select “Create a new STS project in the current solution”.
- Click “Next” and then “Finish”
- Go to the newly created project and copy everything out of the projects App_Code folder into your STS Website in an appropriate place.
- Delete the generated STS Website
- Edit the copied CustomSecurityTokenService and change the GetOutputClaimsIdentity method to the following which will add your users roles as claims to the generated token.
protected override IClaimsIdentity GetOutputClaimsIdentity( IClaimsPrincipal principal, RequestSecurityToken request, Scope scope )
{
var outputIdentity = new ClaimsIdentity();
var user = Membership.Provider.GetUser(principal.Identity.Name, true);
outputIdentity.Claims.Add( new Claim( System.IdentityModel.Claims.ClaimTypes.Name, user.Username ) );
foreach (var role in user.Roles)
{
outputIdentity.Claims.Add( new Claim( Microsoft.IdentityModel.Claims.ClaimTypes.Role, role ) );
}
return outputIdentity;
}
Wire up the CustomSecurityTokenService in the Web.config
Add the following to your STS Website’s web.Config
<appSettings>
<add key="IssuerName" value="PassiveSigninSTS"/>
<add key="SigningCertificateName" value="CN=STSTestCert"/>
<add key="EncryptingCertificateName" value=""/>
</appSettings>
<system.web>
<compilation debug="true" targetFramework="4.0">
<assemblies>
<add assembly="Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
</assemblies>
</compilation>
</system.web>
Use the CustomSecurityTokenService to generate a Token that can be posted back to the Relying website
Create a Token action on the AccountController with a TokenModel and view that uses the CustomSecurityTokenService that presents a Token to a view which is then posted back to the calling website via Javascript.
public class TokenModel
{
public string Wa { get; set; }
public SignInResponseMessage Response { get; set; }
}
public ActionResult Token(TokenModel tokenModel)
{
if (tokenModel.Wa == WSFederationConstants.Actions.SignIn)
{
var securityTokenService = new CustomSecurityTokenService(new CustomSecurityTokenServiceConfiguration());
var requestMessage = (SignInRequestMessage)WSFederationMessage.CreateFromUri( Request.Url );
SignInResponseMessage responseMessage = FederatedPassiveSecurityTokenServiceOperations
.ProcessSignInRequest( requestMessage, User, securityTokenService )
tokenModel.Response = responseMessage;
return View(tokenModel);
}
if (tokenModel.Wa == WSFederationConstants.Actions.SignOut)
{
SignOutRequestMessage signoutRequest = WSFederationMessage.CreateFromUri( Request.Url );
try
{
FormsAuthentication.SignOut();
}
finally
{
SessionAuthenticationModule authenticationModule = FederatedAuthentication.SessionAuthenticationModule;
if (authenticationModule != null)
{
authenticationModule.DeleteSessionTokenCookie();
}
}
if (!string.IsNullOrWhiteSpace(signoutRequest.Reply))
{
return Redirect(signoutRequest.Reply);
}
}
return null;
}
@model <Your namespace>.TokenModel
@{
ViewBag.Title = "Token";
}
@using (Html.BeginForm(null, null, FormMethod.Post, new { @action = Model.Response.BaseUri }))
{
<p>Token.</p>
<input type="hidden" name="wa" value="@Model.Response.Action" />
<input type="hidden" name="wresult" value="@Model.Response.Result" />
<input type="hidden" name="wctx" value="@Model.Response.Context" />
<noscript>
<p>JavaScript is disabled please click Submit to continue.</p>
<input type="submit" value="Submit" />
</noscript>
}
<script language="javascript">window.setTimeout('document.forms[0].submit()', 0);</script>
Linking the STS to your existing membership database
Replace the Membership and Role provider configuration in the STS we.config which would have been auto generated with the configuration from your websites web.config, remember to copy over the membership database connection string.
Get your website to use the STS as an authentication service
We should now be at a point where we can now authenticate against the STS.
- First make your website and the STS use static ports, this will make things easier when working in a team environment.
- Add a reference to the Microsoft.Identity.Model
- Edit your websites config as per the following, you may find that most of this all done for from when we added the STS service reference but you will need to update the address to the STS service. There may also need to be some small differences depending on how complicated your web config is.
<configSections>
<section name="microsoft.identityModel" type="Microsoft.IdentityModel.Configuration.MicrosoftIdentityModelSection, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
</configSections>
<system.web>
<httpRuntime requestValidationMode="2.0" />
<authorization>
<deny users="?" />
</authorization>
<authentication mode="None" />
<compilation debug="true" targetFramework="4.0">
<assemblies>
<add assembly="Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
</assemblies>
</compilation>
<!-- The httpModules will need to be removed when deploying to IIS7-->
<httpModules>
<add name="WSFederationAuthenticationModule" type="Microsoft.IdentityModel.Web.WSFederationAuthenticationModule, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
<add name="SessionAuthenticationModule" type="Microsoft.IdentityModel.Web.SessionAuthenticationModule, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
</httpModules>
</system.web>
<system.webServer>
<modules>
<add name="WSFederationAuthenticationModule" type="Microsoft.IdentityModel.Web.WSFederationAuthenticationModule, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" preCondition="managedHandler" />
<add name="SessionAuthenticationModule" type="Microsoft.IdentityModel.Web.SessionAuthenticationModule, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" preCondition="managedHandler" />
</modules>
</system.webServer>
<microsoft.identityModel>
<service>
<audienceUris>
<add value="http://<website address and port number>/" />
</audienceUris>
<federatedAuthentication>
<wsFederation passiveRedirectEnabled="true" issuer="http://<STS address and port number>/Account/Token" realm="http://<website address and port number>/" requireHttps="false" />
<cookieHandler requireSsl="false" />
</federatedAuthentication>
<applicationService>
<claimTypeRequired>
<!-- If you do not need roles or a name in the claim then remove the following -->
<claimType type="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name" />
<claimType type="http://schemas.microsoft.com/ws/2008/06/identity/claims/role" />
</claimTypeRequired>
</applicationService>
<issuerNameRegistry type="Microsoft.IdentityModel.Tokens.ConfigurationBasedIssuerNameRegistry, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35">
<trustedIssuers>
<!--
You will need to get the thumb print of the Certificate that was created and put in your user store
when Visual Studio generated the STS earlier on
Be careful when copying the thumbprint as you may end up with an invisible funny invalid character
at the front of the thumbprint that you will have to remove.
-->
<add thumbprint="9A7465C17A42099FFB0C061F9D9A7BCBCD440908" name="http://<STS address and port number>/" />
</trustedIssuers>
</issuerNameRegistry>
</service>
</microsoft.identityModel>
Change your websites logout method to do the following
FederatedAuthentication.SessionAuthenticationModule.SignOut();
WSFederationAuthenticationModule authModule = FederatedAuthentication.WSFederationAuthenticationModule;
string signoutUrl = (WSFederationAuthenticationModule
.GetFederationPassiveSignOutUrl(authModule.Issuer, authModule.Realm, null));
return Redirect(signoutUrl);
Fire up both websites and try navigating to your website. If everything is configured correctly you should be redirected to your STS where you can authenticate using the existing membership database and you should then be redirected back to your website with a claim containing your username and roles.
Additional tasks
If we have got here then hopefully everything above worked, here are some additional task that I had to do which are specific to my domain but they are worth considering.
General clean up: The above code works, great, but it is not the prettiest and was simplified for brevity. Consider moving things around to make everything more testable and readable. I created wrappers for certain things so that I could use dependency injection.
Styling and user management: All the above does is move over the authentication to the STS, there is no user management. There is no reason why it cannot stay in the main website, this is of course a decision you will need to make.
Consider how you will deploy: You will need to consider how you are going to deploy this out to other development machines and environments. How are you going to manage certificates and such?
Development and testing: In order to facility testing and prevent the need to have both websites running in a development environment I created a Test STS that allowed me to login as a verity of different users with different claims which I then hosted on an internal server that the whole team could access. In stead of having to enter a username and password all you did was change the name of the user if you wanted and tick the roles you wanted to be in the claim.
Abstracting from a single STS using Azure’s ACS
In order to further separate your website you can also consider using an Access Control Service (ACS) which Microsoft hosts on Azure. The service allows you to sign in from a verity of different Security Token Services and maps the claims from each of these STS Domains to claims your own domain can recognize. You could of course role your own but either of these two topics is a post in its own right.
Further Reading
MSDN: A Guide to Claims-Based Identity and Access Control (2nd Edition)
Windows Identity Foundation
Windows Identity Foundation SDK
Federated Identity with Multiple Partners
Federated Identity with Multiple Partners and Windows Azure Access Control Service
Microsoft.IdentityModel Namespace