Skip to content

Introducing SSO (Single Sign-On) to an existing ASP.NET MVC application

Technology

Apr 10, 2017 - 28 minute read

objectivity-blog-lego
Jaroslaw Szczegielniak
Jaroslaw Szczegielniak
See all Jaroslaw's posts

2988 HC Digital Transformation 476X381

Share

A while ago I was working on a project that required 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 needed 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 they could be solved using a single sign-on (SSO) mechanism based on Thinktecture Identity Server v3 and OpenId Connect protocol. I have written “could” because the original solution was in fact built using the SAML2P protocol – this article will describe how the same problem could be addressed using different, more “modern” technology.

Single Sign-on (SSO) Principles

OpenID Connect, WS-Federation and SAML2P all provide ways of doing single sign-on, 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.
OpenID Connect is the youngest of the mentioned protocols and it is built upon the OAuth2 authorization protocol. It is worth remembering that OAuth2 itself does not support authentication, what in the past led to several custom – and incompatible – authentication implementations introduced by companies as Google, Facebook, Twitter etc. OpenID Connect was designed to eventually replace those custom solutions with a single and secure one, but for now, we ended up with one more protocol, only not a proprietary one.
The following picture presents the common overall idea behind basic SSO between web
applications:

SSO and implicit redirection to Login Page

The diagram presents the basic steps of the SSO process for web applications using implicit OpenID Connect flow (and also passive redirection mechanism in WS-Federation / SAML).

The presented diagram contains 4 main components:

  • User
  • Two web applications: A and B.
  • OpenID 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:

  1. User requests a page from Application A.
  2. As user is not yet authenticated in Application A, the user is redirected to the Identity Provider.
  3. As user is not yet authenticated in Identity Provider, Identity Provider redirects user to Identity Provider’s login page.
  4. User enters credentials and then is authenticated in the Identity Provider.
  5. Identity Provider creates and sends authentication token to the Application A.
  6. As soon as Application A authenticates User using the received authentication token, the initially requested page A is returned to the user.
  7. So far so good, but there is no real SSO with a single application, so now user requests page B from Application B.
  8. As user is not yet authenticated in Application B, the user is redirected to the Identity Provider.
  9. As user is already authenticated in Identity Provider (in step 4), Identity Provider generates and sends authentication token to Application B.
  10. 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 protocols.

Of all the possible ways of providing a Single Sign-On mechanism, in this article I will focus on OpenID Connect and Thinktecture Identity Server v3, using implicit flow.

Please note that the implicit flow is the simplest OpenID Connect flow, covering only the authentication part required by the SSO mechanism. A detailed description of all the flows and tokens in OpenID Connect / OAuth protocols would be very long and is out of scope of this article, but to avoid confusion it may be helpful to at least mention some terms that are often used when describing OpenID Connect / OAuth solutions though:

  1. Authentication token (id_token) is returned from the OpenID provider and is used only by the requesting client application to confirm identity of current user (and e.g. to create authentication cookie in the client application).
  2. Access token is returned from OpenID / OAuth provider and it can be used by its “bearer” to access some services (determined by token scope) of relying party application in the name of the authorized user. Access token usually expires after time configured in OpenID / OAuth provider.
  3. Refresh token is optionally returned with the Access token and can by using by a relying party to refresh (i.e. obtain new) access token (and new refresh token).
  4. Authorization code is a one-time code that can be used to obtain access token (and refresh token) for the first time in more complex scenarios.
  5. Apart from the implicit flow, where applications do not “talk” to each other directly but rely on browser redirection mechanism, there are 2 additional flows supported by OpenID Connect protocol:
    1. Authorization code flow (where client application obtains authorization code that allows to request access and – optionally - refresh token – directly by the application – i.e. not via browser redirection - from the OpenID Provider).
    2. Hybrid flow (which is kind of a shortcut approach combining both implicit and authorization code flows, allowing to obtain authentication token (id_token), access token and authorization code in a single step).

Please bear in mind that in this article we will use only the authentication part of the OpenID Connect protocol so the authentication token (id_token) obtained via implicit flow will be the only token used in the following description. Other flows (and tokens) could be handy when implementing web SPA applications or “thick” / mobile applications, but this not was the case in the described example.

Application landscape and requirements

There were four main components in this project:

  1. An existing front-office application that consists of several modules that work together to allow users to search, read and manage client information.
  2. 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.
  3. 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.
  4. 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 OpenID Connect protocol for authentication.

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 login 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: the basic steps of the SSO process for web applications using implicit OpenID Connect flow

OpenID Connect and Thinktecture to the rescue

In our case we were only interested in the features of OpenID Connect and Thinktecture Identity Server:

  • Single Sign–Onfrom a number of web applications to a single Identity Provider (using implicit OpenID Connect flow).
  • Single Sign–Out; sign-out initiated from any of the “federated” applications will cause the user to be signed-out from all 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 OpenID Connect has the same meaning as in WS-Federation, and as Service Provider has in SAML protocols.
  • Identity provider (a.k.a. OpenID Provider)– it is a system that provides identity to a relying party.

Now the only thing we are missing is the Identity Provider. In theory, 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. But as far as security is concerned, implementing the solution from scratch is almost never a good idea.

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).

Thinktecture IdentityServer is a framework and a hostable component that allows implementing single sign-on and access control for modern web applications and APIs using protocols like OpenID Connect and OAuth2, written in .NET 4.5.x on top on Microsoft.Owin interface libraries.

Theory of SSO

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 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:

Simplified authentication in implicit SSO protocols Here is the description of what happens:

  1. The user’s browser requests a page from the Web Application.
  2. The Web Application checks whether the user is already logged in (technically speaking it checks if an authentication cookie is present).
  3. As there is no cookie, Owin authentication mechanism (configured to use OpenID Connect) kicks in and redirects the user to the Identity Server (HTTP 302 code) with the authentication request.
  4. The user’s browser responds to the redirect by requesting the resource from the Identity Server (it is a “regular” HTTP GET).
  5. Thinktecture Identity Server handles the authentication request, where one of the steps is checking if user is already logged into the Identity Server (i.e. there is an associated cookie present).
  6. If user is not yet authenticated in the Identity Server, another redirection is requested – this time to Identity Server’s login page.
  7. The user enters his/her credentials. The user stays on the page until the credentials are correct – 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. Please note that we didn’t use the “user consent” view here – so the Identity Server does not present the user screen with information what data will be shared (via token) with the requesting client application. While this is a standard mechanism used in OpenID Connect / OAuth2 configurations, it is not really required in the enterprise environment where all components (Identity Server and client "relying party" applications) are managed and secured by a single organization.
  8. When user has entered credentials correctly or is already logged in, the authentication token (id_token) is created by the Identity Server. The claims included in the token match the scopes included in the authentication request (e.g. apart from user id and protocol information, the token may contain information about user profile, e-mail, roles etc.). When created, the id_token is sent to the original Web Application using HTTP POST method (alongside the authentication cookie of the Identity Provider). Please note while OpenID Connect protocol allow for various token formats, in this particular case a JWT (JSON Web Token) was used.
  9. The POST call is handled by the Microsoft.Owin libraries configured for OpenID Connect protocol 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 the 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 possible two alternative variants to the described scenario:

  1. 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 OpenID Connect will execute.
  2. When the user is not yet authenticated in the Web Application, but he/she is already authenticated in the Identity Provider (e.g. because the user 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 is hosted (Identity Server or relying party). In our case, all applications will share a domain address, but this is in no way required. In general, the only requirement is that the user browser has access to both applications (Identity Server and Relying Party Web Application) – the connection between applications is not needed.

These redirections are in fact at the heart of the implicit flow – also known as “passive redirection” mechanism in WS-Federation (passive, because your application does not talk directly to the Identity Provider, nor Identity Provider “talks back” directly to relying parties – all communication is performed using a browser on the user’s workstation). As mentioned at the beginning of this article, as an alternative you can always consider more complex solutions – e.g. authorization code flow - where the client directly asks 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/er.

What about Federated Sign-Out?

Federated Sign-Out is particularly important where users are sharing browsers. Consider this series of events:

  1. User (let’s call him John) opens a new session in a browser and enters Url to the front-office application.
  2. 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.
  3. 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.
  4. 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.
  5. 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 a few minute break.

What will happen when Mary, using the browser, enters any Url of the reporting system? 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)).

Step# Description Session cookies
1 User enters front-office application Url
2 User is redirected to the requested front-office page after authentication
  • Identity Server\auth. cookie
  • front-office\auth. cookie
3 User is presented with the back-office page with client details
  • Identity Server\auth. cookie
  • front-office\auth. cookie
  • back office\auth. cookie
4 User is presented with the report from the reporting system
  • Identity Server\auth. cookie
  • front-office\auth. cookie
  • back office\auth. cookie
  • reporting system\auth. cookie
5 User logs out from the system

If you do not know why new cookies are added, please go back to 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 (id_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?
In the previous version of Identity Server and WS-Federation protocol the server was storing information about all “authenticated” applications in a single SSO session, so it was quite simple – each application instructed by Thinktecture Logout View removed its own cookie:

Logout from iframe in Thinktecture

As it turned out, Thinktecture Identity Server v2 used yet another cookie just to store information about each relying party that used it to authenticate its user. So when user wanted to log out, the following steps took place:

  • The application (relying party) from which user initiated Log Out called the FederatedSignOut() method of the WSFederationAuthenticationModule static class in its MVC controller.This caused the user’s browser to be redirected to the Thinktecture Identity Server v2 sign out page.
  • When Thinktecture Identity Server's sign out page was returned to the browser, it:
    • Removed the Thinktecture authentication cookie.
    • Contained several 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 was recognized up by WS-Federation http module of each application – and the module ensured removal of its authentication cookie.

And that was all. With OpenID Connect protocol the case is a little more complicated – mostly because this protocol was invented to handle many different types of client applications (web, mobile, WinForms, WPF etc.) and the idea is reversed - the Identity Provider has no “power” to force client relying party applications to terminate their sessions when Identity Provider’s session expires (or is ended during logout).

Nevertheless, there are 3 levels of logout support in OpenID Connect solution:

  1. When user logs out from the relying party application, it does not affect the user’s session in the Identity Provider.
  2. When user logs out from the relying party application, it ensures that user’s session in Identity Provider is ended as well (but it does not affect user’s sessions in other applications that used the same Identity Provider for authentication).
  3. When user logs out from the relying party application, it ensures that:
    1. User’s session in Identity Provider is terminated.
    2. User’s sessions in all other relying party applications are ended as well.

The 1 level does not require any additional explanation, but it usually is not expected in enterprise solutions (as it leads to the scenario described at the beginning of this section). One could wander if such a scenario is ever expected – but considering roots of OpenID Connect protocol - i.e. world of independent internet applications connected using single identity provider (e.g. Google) - it makes sense.

The level 2 is a little better, but it still does not prevent us from the described scenario. Technically, to start client initiated logout, all that application needs to do is to send a proper request to the end session endpoint of Identity Provider. To make the solution more “fool-proof” it is recommended to configure Identity Provider in such a way that it requires original id_token to be included in the request – it will ensure that only a previously authenticated client can end the Identity Provider session.

The level 3 extends the level 2 and requires most “custom” work – partly because the OpenID Connect Session Management specification is still in a draft version, so it may not be fully supported by existing tools and libraries.

The overall flow in such a solution may look as follows:

Idp cookie removal interactions Please note that only steps and cookies relevant to the Single Log Out process are included there. Steps can be described as follows:

  1. Application 1 requests user authentication.
  2. After logging in, the Idp Cookie is returned to the browser (in addition to the Application 1 authentication cookie of course).
  3. Application 2 requests user authentication.
  4. No logging in is required (as Idp Cookie is still present in the browser). Authentication cookie for Application 2 is created.
  5. User of the Application 1 requests logout.
  6. Identity Provider ends user session and removes the Idp Cookie. As soon as response is returned, user’s session in Application 1 is ended as well.
  7. And now the magic happens: both Application 1 and Application 2 contains (hidden) IFrames with src set to the Identity Provider view so they have access to Idp Cookie. JavaScript code inside the IFrames checks if the Idp Cookie is present in configured intervals (e.g. every 30 seconds). As soon as code detects that the cookie is no longer present (code in IFrame of Application 2 in this case), JavaScript event is sent to the hosting page of Application 2. This event should be handled by initiating logout from the Application 2.

Please note that because of the nature of this mechanism, there is no guarantee when (and even if) session is ended in all connected applications (e.g. because of an error). This means that still the only way to be sure that the session is properly ended is to close the browser window (or all of them for browsers that share cookies between windows).

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 is 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’ id_token and prolonging it, if necessary.

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:

  1. 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.
  2. 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 the next 60 minutes. The user’s session in application A will be valid for the next 45 minutes (because of 15 minutes of being idle).
  3. 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.
  4. 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 can 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 log in 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, s/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: Sliding expiration in SSO with Thinktecture

For “Sliding Expiration per Identity Provider” to work, we should configure the Identity Server in a particular way:

  1. Each Relying Party Application’s authentication cookie lifetime is always set to a fixed duration (so no sliding expiration takes place there) – represented by the appCookieDuration parameter in the diagram above.
  2. The authentication is “sliding” only on the Identity Provider side. The lifetime of this cookie is controlled by the IdentityServer3.Core.Configuration.CookieOptions instance used by the Owin.UseIdenitytServerExtension class and represented by the idpCookieDuration parameter in the diagram above.
  3. For sliding expiration to work the idpCookieDuration should always be longer than appCookieDuration (please see the example below for more explanation).

It may be easier to describe this using the following (simplified) example for parameters: idpCookieDuration = 1 hour(i.e. user’s session duration in Identity Provider application) and appCookieDuration = 30 minutes(i.e. user’s session duration in Relying Party application):

  1. At 12:00 the user is authenticated so Identity Provider creates an idpCookie with its expiration set to 13:00. As soon as the id_token is obtained, the relying party application creates its own authentication cookie (appCookie) set to expire at 12:30.
  2. All user's requests to the relying party application before 12:30 are authenticated locally (i.e. no communication with Identity Provider takes place).
  3. The user's 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 idpCookie is still valid, the user is not redirected to the login page, and a new authentication token (id_token) is returned to the relying party application. At the same time (in Identity Provider) idpCookie’s lifetime is extended – using the idpCookieDuration value of 1 hour – so new idpCookie expiration time is set to 14:05.
    After the id_token is returned to the relying party, the application sets its appCookie to expire in 30 minutes, i.e. at 13:05.
  4. If the next user's action takes place at 14:06 both application authentication cookie and Identity Provider’s cookie are expired – then the user will be redirected to the login page.

The above description is simplified. In practice, there are a number of additional factors:

  1. Clock time differences: these differences could cause issues with WS-Federation and SAML protocols – because lifetime of authentication token is set by the Identity Provider. In OpenId Connect protocol id_token does not have expiration date and the relying party controls its own session lifetime so such issues should not occur.
  2. Sliding expiration “holes”:
    1. For the sliding effect of the idpCookie 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 idpCookieDuration = 60 minutes and appCookieDuration = 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 coffee for the next 30 minutes, the user session in Identity Provider will expire 60 minutes after his/her login and not 60 minutes after his/her last action in relying party application. In theory, this effect could be eliminated by using a short appCookieDuration 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.
    2. To make this even more complicated, the same is probably true for the idpCookie – so extension of the idpCookie lifetime will not take place in the first half of the idpCookie lifetime duration.

Please make sure that you add proper CookieOptions instance to the AuthenticationOptions in the Owin.IdentityServerExtension class, UseIdentityServer method to ensure that the sliding expiration is active.

Issues with JavaScript and POSTing tokens

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 – no matter if the requests refer to the whole browser or an IFrame. Please 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 from the one in “host” page URL.

A situation becomes a bit trickier when we extensively use Java Script and AJAX requests in our application and – for whatever reason – we would like to obtain token from Identity Provider using HTTP POST, not HTTP GET method. While in case of WS-Federation there was no choice (most likely because tokens were XML documents so they could get quite large), OAuth2 and OpenID Connect supports returning tokens using HTTP GET (via redirection).

In some cases tokens can get too large for a query string (what in general should be avoided) and then we may need to rely on HTTP POST even with OpenID Connect protocol, what can lead to some JavaScript related issues.

Let’s consider the following example:

  1. User logs in to the (relying party) application A, via Identity Provider.
  2. System A is written as an “SPA” (Web Single Page Application) – after loading initial page (MVC view), all further communication takes place between JavaScript code hosted in this initial page and a number of Web API methods.
  3. 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.
  4. After 31 minutes, one of the JavaScript methods (getCustomers) in the application A calls Web API method (GetCustomers). This is what happens:
    1. getCustomers initiates a request to http://superdomain.com/ApplicationA/WebAPI/GetCustomers.
    2. The authentication cookie in application A is already expired so the request is redirected with code 302 to the Identity Provider, e.g. http://evenbetterdomain.com/idp/core/connect/authorize.
    3. 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.
    4. And… nothing happens – request ends here and – in particular no customer data is presented to the user.

Why? The main issue lies with the POST response – there is no such thing as “redirect to POST” response code. What Identity Provider returned in step c. above is in fact a regular (code: 200) HTML response with embedded JavaScript method to perform the POST. The generated HTML may look similar to the following snipped (this particular example is actually taken from the WS-Federation Identity Provider, but the key fragment is the bolded script tag):

WS-Federation Identity Provider

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.

The problem is that when such a document is returned to the JavaScript caller – as a string – the script included in the document is not executed.

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.

In practice there are at least a number of ways to avoid this issue. We can for example:

  1. Use both access token and refresh token (to obtain new access token when old expires). Please note that this solution will work only in “pure” SPA solutions – as for predictable user experience it requires JavaScript to be involved in every service call after logging in. In particular, it may not be the best option if our “Relying Party” application is a mix of regular HTML and rich “SPA” pages.
  2. Ensure that tokens are returned in the query string of the URL – via regular redirection - and that the session expiration timeout is correctly configured on both Identity Provider and Relying Party sites. In such a case if user is still logged into the Identity Provider - after a number of “30*” redirections - the caller should receive requested response. We would still need to handle the situation when user’s session in Identity Provider is already expired though – e.g. in a way described below.

  So if the first option is not applicable, it is still (relatively) easy to solve both of the described issues when we use “Sliding expiration per application”. In such a case – i.e. when we have sliding expiration on the relying party side – the chance that authentication will expire during intensive user interaction is pretty small so there is nothing stopping us from a standard solution where we change how the action / WebAPI method behaves when a user, who is not authenticated, is trying to call it. As long as we call our own application, we change the behavior to return HTTP code 401 (not found) instead of 302 for all actions that are designed to be called by Java Script code (in ASP.NET MVC application it can be done by replacing standard AuthorizeAttribute with custom extended version that uses HttpRequestBase.IsAjaxRequest() method to determine its behavior). Of course we need to handle this response in JavaScript – but as nothing happens “under the hood” – we have a full control and we may instruct our JavaScript code to always fall back to full redirect to the current page whenever code 401 is returned. In case of SSO, it will mean that a user will either be redirected to the Identity Provider’s login page or the authentication cookie will be automatically generated and the user will stay on the current page. In any case, the JavaScript call will not succeed and will need to be once again initiated by the user.

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 an unpleasant experience for a user to have whatever work they are doing with the system interrupted each 30 minutes.

I don’t really know any elegant solution to this issue, but I’m far from being a JavaScript expert – so I would really appreciate any suggestions in this area.

I was able to come up with an initial draft of one solution that seems to be working, however, it is far from being recommended.

The idea can be summarized in a few steps:

  1. In the JavaScript callback method that we use to handle the JSON response from WebAPI method, we need to detect what kind of response it is:
    1. A valid JSON document.
    2. A HTML response from Identity Provider (with an authentication token to POST).
    3. Any other HTML response.
  2. 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.
  3. If it is a HTML response from Identity Provider (with an authentication token to POST), we do some (ugly) “magic”:
    1. We load the response to the hidden IFRAME.
    2. We wait for the IFRAME to finish the loading. The embedded JavaScript code, and POST that will create new authentication cookie, should be completed by then. If this is the case, the IFRAME will be redirected to the originally requested URL – i.e. the WebAPI method.
    3. 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.
  4. If it is any other HTML response, force full 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.

In theory, it should work; in practice, I have checked that it works at least in some of the browsers ;-).

Summary

In this entry I have tried to share some of my thoughts related to switching a set of existing ASP.NET MVC applications from regular authentication to Single-sign-On (SSO), considering OpenID Connect and Thinktecture Identity Server v3 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 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. As soon as you understand the big picture, you can pick those tools and protocols that suits you best.

(Original version of this article was first published on Simple-Talk)

2988 HC Digital Transformation 476X381
Jaroslaw Szczegielniak
Jaroslaw Szczegielniak
See all Jaroslaw's posts

Related posts

You might be also interested in

Contact

Start your project with Objectivity

CTA Pattern - Contact - Middle