I’m currently working on a project that requires us to integrate an existing ASP.NET MVC application with a number of new systems, both back- and front-office. The user would like them all to work together as if it were one integrated application, and a key requirement is that there should be a single sign-on (SSO) for all the web systems.
Users will need to be able to navigate between pages of any or all of these applications without the tiresome chore of repeated authentication.
In this article I would like to share some of the problems that we faced, and the way we solved them when we designed and implemented single sign-on, based on .NET WIF, Thinktecture Identity Server v2 and WS-Federation protocol, as part of this integration project.
Single Sign-on Principles
WS-Federation, SAML2P, OpenID and OAuth all provide ways of doing single sign-on (SSO), with a similar general principle. If you understand how one of them works under the hood, it is easy to grasp any of the others.
The following picture presents the common overall idea behind basic SSO – at least as far as we are talking about web applications:
The diagram presents the basic steps of the SSO process for web applications using passive redirection mechanism. The term “passive” reflects the fact, that applications that are involved in the process do not communicate directly with each other, but rely on browser’s redirection and standard HTTP GET and POST messages. We can refer to “active SSO” when a relying party application talks directly (e.g. via a Web Service) to the Identity Provider to validate the user’s identity and obtain the related security token: Such direct communication is required when we have “thick” relying parties (e.g. regular Windows applications). With some effort we could use the “active SSO” in web applications as well, but it is the “passive redirection” route that is a foundation of the web SSO standards I know of, so this is the mechanism on which I will focus in this article.
On the presented diagram we have 4 main elements:
- Two web applications: A and B.
- Identity Provider (that – technically – is also a web application, i.e. it accepts regular HTTP requests).
Both applications (A & B) are configured to use a single Identity Provider – so when user logs into one of the applications, he (or she) should have the perception that he (or she) is already logged in to the second one (thus: Single Sign On).
To achieve this perception, the following steps take place:
- User requests a page from Application A.
- As user is not yet authenticated in Application A, the user is redirected to the Identity Provider.
- As user is not yet authenticated in Identity Provider, Identity Provider redirects user to Identity Provider’s login page.
- User enters credentials and then is authenticated in the Identity Provider.
- Identity Provider creates and sends authentication token to the Application A.
- As soon as Application A authenticates User using the received authentication token, the initially requested page A is returned to the user.
- So far so good, but there is no real SSO with a single application, so now user requests page B from Application B.
- As user is not yet authenticated in Application B, the user is redirected to the Identity Provider.
- As user is already authenticated in Identity Provider (in step 4), Identity Provider generates and sends authentication token to Application B.
- User becomes authenticated in Application B and page B can be returned to the user.
This presents the high level sequence that is quite common in various SSO standards.
Of all the possible ways of providing a Single Sign-On, I will, in this article, focus on WS-Federation and Thinktecture Identity Server v2. I will also only mention features of WS-Federation that are relevant to our solution. If you are interested in more details, you can read a good introduction to WS-Federation on MSDN.
The project mentioned in this article does not need a full blown trust federation, in the sense of a number of independent identity services / providers that trust each other: All of the systems are hosted in a single domain and we wanted to have only one Identity Provider, so there are no trust issues here. The great thing about WS-Federation though is that its support is part of the Windows Identity Foundation (WIF), which – starting from the .NET 4.5 – is already built into the .NET Framework. Thanks to that, there is no significant overhead in using WS-Federation in .NET application, in source code at least. It is fairly easy once you know you way around it.
Application landscape and requirements
There were four main components in this project:
- An existing front-office application that consists of several modules: These work together to allow users to search, read and manage client information.
- An existing application, the management console,that includes the management of access rights for front-office application users to such components as menus and visible links.
- A new back-office system that will be responsible for storing and processing information that is accessed mostly via the existing front-office application (I will elaborate later about this).It will be the only place where user-details and credentials will be stored. The Back-office system will consist of two main elements: A Website for real users, and SOAP Services for applications.
- A new reporting system,which will extend the data presentation capabilities of the existing front-office application. This system is, in fact, another web application.
All applications / systems will be hosted in our client’s environment. Except for the management console, they will all be accessible from both internet and Intranet. The management console will be accessible only from the Intranet.
We can change the existing applications, the front-office and management console, but we do not have any access to, or control over, the source code of either the back-office system or the reporting system. All the new systems, back-office and reporting, can be configured to use the WS-Federation protocol for authentication, unlike most Java systems which tend to support only SAML protocols for federation purposes.
Initially both the existing applications, front-office and management console, used their own independent ASP.NET Membership Providers. This means that there was no single sign-on (SSO) between those applications.
Our customer needed to:
- Present selected views (pages) of the new back-office system and reporting system inside an existing front-office application (through IFrames).
- Introduce a Single Sign-On mechanism to avoid repeat logon when navigating between different systems.
- Ensure that the style of the login page is consistent with the existing front-office application.
- Have users’ credentials stored ONLY in the new back-office system. This system will provide services required to validate username and password to the external applications.
- Keep the authorization mechanisms for each system in place – SSO should be used only for authentication. The reason for that is that the authorization requirements vary greatly between applications and there is no apparent business case for justifying changes in this area.
Taking all this into account, here is how expected solution should look like:
WS-Federation and Thinktecture to the rescue
In our case, as we didn’t need a full-blown trust federation, we were only interested in the following features of WS-Federation:
- Single Sign–On from a number of web applications using passive redirection to a single Identity Provider. I will explain in more detail how passive redirection works and why it is called “passive” in the next section.
- Federated Sign–Out; sign-out initiated from any of the “federated” applications will cause the user to be signed-out from all of the other applications into which he is logged in during a single session.
Additionally it may become handy to familiarize yourself with a few terms:
- Relying party – it is any application that relies on external service to authenticate its users.Every application mentioned in our example (i.e. front-office, management console, back-office and reporting system) is a relying party. Basically Relying party in WS-Federation has the same meaning that Service Provider has in SAML protocols.
- Identity provider – it is a system that provides identity to a relying party.
- Realm – it is a very generic term related to independent “security realms“. In our case they are simple string values representing the “home” url of each of the applications (including identity provider).
Now the only thing we are missing is the Identity Provider. We could implement it from scratch, especially as we do not want our identity provider to store and manage our users information, but these should be accessed via a web service from our new back-office system. After all, WIF have all the component parts in place to help to provide protocols and communication.
Luckily there is no need to do it as we have a very nice open-source alternative – i.e. Thinktecture Identity Server (please take a look at its documentation and source code if interested). We decided to use version 2 as it was preferred by our customer – version 3 was still in beta at that time.
Thinktecture Identity Server is a light-weight Security Token Service (STS), written in .NET 4.5, ASP.NET MVC 4, WCF and Web API – supporting a number of “popular” security protocols, including WS-Trust and WS-Federation. Unfortunately one of the shortcomings is lack of support for SAML 2 protocols – they are quite popular, especially in the Java world. As far as SAML goes, the product supports SAML 1.1 and SAML 2.0 tokens, but SAML tokens specification is just a small part of the whole SAML specification.
Theory of single sign-on
Having all pieces in place, we can start working on the solution. At the beginning it is quite important to acquire a basic understanding of how all these elements interact with each other. In our case it is even more important since we already have two working applications (the front-office and management console) and we do not want to break anything. Unfortunately this is possible due to the surprising side-effects of SSO – even though, initially, the situation may seem obvious – more on that later.
Let’s take a look at the diagram (still simplified, but more technical this time) that depicts how SSO with passive redirect works when the user tries to access a page of the Web Application for the first time:
Here is description of what happens:
- The user’s browser requests a page from the Web Application.
- The Web Application checks whether the user is already logged in (technically speaking it checks if an authentication cookie is present).
- As there is no cookie, WS-Federation authentication module kicks in and redirects the user to the Identity Server (HTTP 30x code).
- The user’s browser responds to the redirect by requesting the resource from the Identity Server (it is still a “regular” HTTP GET for passive redirection).
- Thinktecture Identity Server works as a regular ASP.NET application here and checks whether the user is already authenticated (i.e. has a valid authentication cookie for Identity Server).
- If user is not yet authenticated on Identity Server, another redirection is requested – this time to Identity Server’s login page. In Thinktecture’s implementation of Identity Server, this is enforced by the standard Forms authentication.
- The user enters his credentials. He stays on the page until he does it right – no information about “invalid login attempt” is returned to the original Web Application – even when the user account is eventually blocked, e.g. after too many tries. The only way to move the process forward is to provide valid user credentials.
- When user has entered credentials correctly, or is already logged in, the security token is created by the Identity Server. By default, the token consists of very basic information, known as a ‘claim’, such as user name, authentication date and time, but it may be easily extended to include other information such as roles or e-mail address.When created, the token is sent to the original Web Application using HTTP POST method (alongside the authentication cookie of the Identity Provider). In this particular case a SAML token was used, but JWT (JSON Web Token) format is supported as well.
- The POST call is handed by the WSFederationAuthenticationModule in the Web Application – a valid ClaimsPrincipal with ClaimsIdentity object is created and set as a current user. Additionally a “regular” ASP.NET authentication cookie is created for a Web Application.When done, the authentication step in the Web Application is finished, and subsequent steps are performed.One of these steps is authorization – it may use claims returned from Identity Provider, but we stayed with our implementation of a custom Role Provider that loads user roles from an external service.Once authorization is finished, the requested page is finally returned to the user (with new Web Application authentication cookie).
There are two main variants:
- When the user is already authenticated in the Web Application, nothing special will happen – the standard ASP.NET authentication mechanism will render the response and no functionality related to the WS-Federation will execute.
- When the user is not yet authenticated in the Web Application, but he is already authenticated in Identity Provider (e.g. because he was logged in when was using another application with the same Identity Provider in the same session).In this case the only difference is that right after step 5, we jump to step 8.
Because the redirections are used, there is no limitation on where each of the involved applications are hosted (Identity Server or relying party). In our case, all applications will share a domain address, but this is in no way required.
These redirections are in fact at the heart of the passive redirection mechanism (passive, because your application does not talk directly to the Identity Provider, nor Identity Provider “talks back” directly to relying parties – all communication in performed using a browser on the user’s workstation). As mentioned in the beginning of this article, as an alternative you can always consider active authentication, where the client directly asks STS / Identity Provider to get a valid authentication token (via Web Service / Web API).
Unexpected features of SSO
With SSO you have the important problem that a single sign-on will expose authentication weaknesses in your existing applications. Every application will need to check separately what should be visible to individual authorized users. The identity provider merely confirms that the users are who they claim to be.
This caused problems with our project. Just after a “regular” authentication was replaced with the Single Sign-On, a bug was raised by testers that an administration console user that should not have any access to the front-office application was able to open it.
The issue of course was not caused by the SSO itself – it was rooted in the authorization process in the front-office application, but I think that this is quite a common case that in an application not yet configured for SSO, some basic features are available for all authenticated users.
When you introduce SSO, you should use it very carefully. This is especially true if you do not have full control over who is authenticated by Identity Provider. Authentication just tells you the information about the identity of the user – you need to check separately in every application what should be visible to him.
What about Federated Sign-Out?
Federated Sign-Out is particularly important where users are sharing browsers. Consider this series of events:
- User (let’s call him John) opens new session in a browser and enters Url to the front-office application.
- Next, he is redirected to the login page (note that this page is hosted in Identity Server, not the front-office application itself) where he successfully enters his credentials and is redirected to the front-office main page.
- There he looks up one of his clients and selects “show client details” option. In effect he is redirected to a new front-office page, where (in an IFrame) he is presented with client details page of the back-office system.
- John selects another option called “client performance in last 12 months”. Yet another IFrame is presented to him inside the front-office application, this time its contents will be the client performance report in the reporting system.
- John decides that it is now home-time, so he logs out from the front-office application and he goes home. He does not close the browser and does not switch off the computer – as he wants to leave the computer ready for use by Mary who has the second shift and starts work on the same workstation after few minutes break.
What will happen when Mary enters in the browser any reporting system Url? Without Federated Sign-Out, she would be able to get to this page still logged in as John.
So how does Federated Sign-Out prevent this from happening? Let’s analyze the same series of events, but this time at a more technical level (the first column describes the step and the second one specifies session cookies in user browser after that step (selected only – relevant to the scenario)).
User enters front-office application Url
User is redirected to the requested front-office page after authentication
User is presented with the back-office page with client details
User is presented with the report from the reporting system
User logs out from the system
If you do not know why new cookies are added, please look back at the Theory of Single Sign-On section. In each of those steps the following actions take place:
- Redirection to the Thinktecture Identity Server.
- Check of user authentication status.
- POST from Identity Server to original application with authentication token and creation of authentication cookie for a particular application – regardless of the way we entered application boundaries (directly, through redirection from other application or from an IFrame).
But how it is possible that when user logs out, all cookies are removed from the browser – after all (normally) no site can remove cookies of any other site? Actually the mechanism is very clever in its simplicity – each application removes its own cookie – and here it is shown how:
As it turns out, Thinktecture Identity Server uses yet another cookie just to store information about each relying party that used it to authenticate its user. So when user wants to log out, the following steps take place:
- The application (relying party) from which user initiated Log Out calls the FederatedSignOut() method of the WSFederationAuthenticationModule static class in its MVC controller.This causes the user’s browser to be redirected to the Thinktecture Identity Server sign out page.
- When Thinktecture Identity Server sign out page is returned to the browser, it:
- Removes the Thinktecture authentication cookie.
- Contains a number of hidden IFrames – one per each relying party for which an authentication token was generated. The source attribute of each IFrame is equal to relying party realm Url extended with parameter: wa=wsignoutcleanup1.0. This parameter is recognized up by WS-Federation http module of each application – and the module adds to the response instructions to remove its authentication cookie.
And that is all.
Note that – similarly to the Single Sign-On case – we need to consider that we have no control over where the sign out will be initiated. It means that if – for example in our front-office application – we have some cleanup code in our Logout controller action, it will be executed ONLY when the logout was initiated in the front-office. When logout is initiated from the management console, you still will be logged out from the front-office application if needed – but the front-office cleanup code will not be executed. To avoid such a situation you should not rely on actions of your controller to handle sign out cleanup – you should switch to the FederatedAuthentication.WSFederationAuthenticationModule.SignedOut event (and usually the best place to bind this event with your code is Global.asax and Application_Start method).
Sessions and sliding expiration
Usually – especially in enterprise applications – we don’t want a user to be logged in indefinitely into our system. It means that we will use session cookies rather than permanent cookies for authentication. What is more there are security reasons for automatically logging out users that have been inactive for more than a configurable amount of time (e.g after 30 or 60 minutes).
In regular ASP.NET applications we did this by simply configuring sliding expiration on the authentication cookie. If there are a number of applications connected to a single Identity Provider it is a bit more complex though.
We have basically (at least) two options:
- Sliding expiration “per application”.
- Sliding expiration “per Identity Provider”.
In the first option (Sliding expiration “per application”) we can set the duration of the authentication session for the same time for each application and Identity Provider (e.g. 60 minutes). Each application would then manage the “sliding” of its user session on its own by checking, on every request, the authentication cookie created when Identity Provider returned its’ token and prolonging it if necessary. This is certainly doable (using FederatedAuthentication.SessionAuthenticationModule class methods), but it is not particularly elegant.
There is a problem with this approach because the application that changes the session duration isn’t doing it across all the applications and the identity provider. This will cause problems, as follows:
- A user logs in to the application A (via Identity Provider). At this point the user’s authentication cookie is set to be valid for next 60 minutes.
- After 15 minutes of inactivity the user opens application B (that is configured to use the same Identity Provider). Both the user’s session in Identity Provider and application B is set to be valid for next 60 minutes. The user’s session in application A will be valid for next 45 minutes (because of 15 minutes of being idle).
- For the next 75 minutes the user plays a little with application B (so its cookie is constantly reset to be valid for the next 60 minutes). At the same time the user’s sessions in both Identity Provider and application A have expired.
- Next the user performs an action that redirects this user from page of application B to page of application A. As user’s session in application A is already expired – the browser redirects the user to Identity Provider. As user’s session in Identity Provider is also expired, Identity Provider presents its Login Page.
As we see we don’t really have SSO in this use-case despite using common Identity Provider; and still being logged into application B, the user needs to login again to application A.
One way of solving this issue is to set Identity Provider’s session lifetime to be much longer than the sessions in individual applications (e.g. 8 or 24 hours). This still does not solve the problem though, but merely makes it less likely to occur. It introduces one new problem: Unless the user explicitly logs out from the system using the Federated Sign Out infrastructure described before, she/he may stay logged into Identity Provider (i.e. can access all Relying Parties of this provider) for a very long time.
To avoid these issues, we may use the ‘Sliding Expiration per Identity Provider’ option, which is presented on diagram below:
For“Sliding Expiration per Identity Provider” towork, we have to configure the Identity Server in a particular way.
- Each Relying Party Application’s authentication cookie lifetime (called “local cookie” on the diagram above) is always set to a fixed duration (so no sliding expiration there). This value is configured using the tokenLifeTime parameter in Thinktecture Identity Server v2 configuration for Relying Party.
- The authentication is “sliding” only on the Identity Provider side (using the SsoCookie mentioned in the diagram). The lifetime of this cookie is controlled by the ssoDuration parameter in Thinktecture Identity Server v2 General configuration.
- For sliding expiration to work the ssoDuration should always be longer than tokenLifeTime (please see the example below for more explanation).
It may be easier to describe this using the following (simplified) example for parameters: ssoDuration = 1 hour(i.e. user’s session duration in Identity Provider application) and tokenLifeTime = 30 minutes(i.e. user’s session duration in Relying Party application):
- At 12:00 a user is authenticated so Identity Provider creates an ssoCookie with its expiration set to 13:00 and the token of a relying party application set to expire at 12:30. The relying party application creates its own authentication cookie set to expire at 12:30.
- All user requests to the relying party application before 12:30 are authenticated locally (i.e. no communication with Identity Provider takes place).
- The user request at 12:35 cannot be authenticated locally (the relying party application cookie is expired), so an authentication request is made to the Identity Provider.Because the Identity Provider’s ssoCookie is still valid, the user is not redirected to the login page, and a new authentication token is returned to the relying party application (set to expire in 30 minutes, at 13:05). At the same time (in Identity Provider) ssoCookie lifetime is extended – using the ssoDuration value of 1 hour – so new ssoCookie expiration time is set to 14:05.
- If the next user action takes place at 14:06 both application authentication cookie and Identity Provider’s ssoCookie are expired – then user will be redirected to the login page.
The above description is simplified. In practice, there are a number of additional factors:
- Clock time differences: In reality SSO is quite often established between a number of servers in various locations, so we cannot ensure that all clocks are in sync. To accommodate for this, the maximumClockSkew parameter determines, what the maximum acceptable time difference between different servers should be accepted. By default in WS-Federation in WIF this parameter equals to 5 minutes. Side effect of this parameter – especially when testing both Identity Provider and Relying Party on a single machine – is that if you set ssoDuration (or tokenLifeTime) to 1 hour, the created token will in fact expire after 1 hour and 5 minutes. This seems like a small difference, but if – for testing purposes – you set tokenLifeTime to 1 minute and it does not expire before 6 minutes has passed, you start to wonder what is wrong (usually using quite different words :-)).The value for this parameter is configurable in <system.identityModel> section of web.config.
- Sliding expiration “holes”:
- For the sliding effect of the ssoCookie to take place, an authentication request between a relying party application and Identity Provider needs to take place – and no such request is executed as long as the “local” application authentication cookie is valid. It means, that – for example – having ssoDuration = 60 minutes and tokenLifeTime = 30 minutes, if a user interacts with an relying party application intensively during the first 30 minutes (i.e during the fixed lifetime of the original authentication cookie) and then goes for a coffee for the next 30 minutes, the user session in Identity Provider will expire 60 minutes after his/hers login and not 60 minutes after his/hers last action in relying party application.In theory this effect could be eliminated by using a short tokenLifeTime value (that controls lifetime of authentication cookie in a Relying Party Application), but as it increases the frequency of calls to the Identity Provider, it will cause some performance overhead.
- To make this even more complicated, in the Thinktecture’s Identity Server v2 implementation, ssoCookie expiration extension will not take place in the first half of the ssoCookie lifetime.
Note that sliding expiration is not active by default in the Thinktecture Identity Serverv2 implementation – you need to add the following code to the Global.asax.cs in the Thinktecture.IdentityServer.sln solution, OnPremise\WebSite project:
public override void Init()
All mechanisms described so far – i.e. SSO with passive redirects for both Single Sign On and Federated Sign Out – work just fine when we are talking about regular HTTP requests – regardless if the requests refer to the whole browser or an IFrame. Note, that the IFrame part is only partially true – as long as Identity Provider and Relying Party are in the same domain – because some browsers by default will reject cookies from IFrame if its source domain is different than the one in “host” page URL.
A situation becomes a bit trickier when we extensively use Java Script and AJAX requests in our application. The main issue here is that in WS-Federation, the authentication response is sent from Identity Provider to the Relying Party using HTTP POST – and I was not able to find any way to change it.
Let’s consider the following example:
- User logs in to the (relying party) application A, via Identity Provider.
- The Authentication cookie in system A is set to a fixed lifetime duration of 30 minutes, while Identity Provider’s authentication cookie is configured to expire after 60 minutes, but with a sliding expiration.
- getCustomers initiates a request to
- The authentication cookie in application A is already expired, so the request is redirected with code 302 to the WS-Federation Identity Provider http://evenbetterdomain.com/IdentityProvider/issue.
- The response 302 code is automatically handled by the browser engine and request gets to the Identity Provider. Because the user is still authenticated within the Identity Provider, new authentication token is generated and POST response to the application A is returned to the caller.
- And… nothing happens – request ends here and – in particular no customer data is presented to the user.
- getCustomers initiates a request to
<input type="hidden" id="wa"" value="wsignin1.0" />
<input type="hidden" id="wresult""
value="(... an encoded security token ...)"/>
<input type="hidden" id="wctx""
<p>Script is disabled. Click Submit to continue.</p>
<input type="submit" value="Submit" />
If such a result is returned to a browser, the window.setTimeout method at the end of the document causes immediate submit of the provided form. The form is posted to the Relying Party url (in this case: http://superdomain.com/ApplicationA/). Among other values (set in the hidden input fields), there is a context (input id=”quot;wctx“), which instructs the browser where user should be redirected when the security token is processed – in this case it is the originally requested URL for the WebAPI method: /ApplicationA/WebAPI/GetCustomers.
Actually the very similar behavior occurs when the user is no longer authenticated in the Identity Provider and a redirect to the Login Page is required – in such a case the rendered Login Page HTML is returned to the caller – and again it is not what one would expect.
A much more complicated problem is related to “Sliding Expiration per Identity Provider” though. In such a case, the local relying party authentication cookie will expire every fixed period (e.g. every 30 minutes), so it would be rather unpleasant experience for a user to have whatever work they are doing with the system interrupted each 30 minutes.
I was able to come up with an initial draft of one solution that, while seems to be working, is far from being one to be recommended.
The idea can be summarized in a few steps:
- A valid JSON document.
- A HTML response from Identity Provider (with an authentication token to POST).
- Any other HTML response.
- If it is a JSON, we treat it as a valid response and so we do whatever we need to do – i.e. the application continues its normal flow (and scenario ends here).
- If it is a HTML response from Identity Provider (with an authentication token to POST), we do some (ugly) “magic”:
- We load the response to the hidden IFRAME.
- We check contents of this hidden IFRAME.If this is a valid JSON response, take its value and go to the step 2.If it is not, go to step 4.
- If it is any other HTML response (or in general – any other response that the previously mentioned) force full page refresh of the current page.
This will cause redirect to the Identity Provider – if the invalid response was caused by expired authentication – or will simply refresh the current page (if this was caused by some other issue). It may be reasonable to add some custom error message when it is the latter.
I was able to write some code that proves that such an approach can really work – as the code is far from being elegant, I don’t really want to include it here, but if anyone is interested – just let me know, I will be glad to share it with you.
In this article I have tried to share with you my experience of switching a set of existing ASP.NET MVC applications from regular authentication to Single-sign-On (SSO) with WS-Federation and Thinktecture Identity Server v2 as Identity Provider.
The topic is very broad so I didn’t want to get into too much details – especially related to customization, installation and configuration of Thinktecture Identity Server v2 and Relying Party applications. Personally I think that when you are starting with a SSO implementation in your applications, it is most important to first understand the big picture of the underlying technology and dependencies. Without this broad understanding, you can easily get lost in details without any idea why something is not working as you expected it to work – but if you understand the big picture then you can pick those tools and protocols that best suits you.
If there is any interest in Single-Sign-on, I will be glad to continue with some more in-depth technical articles, focusing on selected issues. In any case, I hope that you have found at least some interesting and useful information for your projects here in this article.