Reading Time: 4 minutes

SSO without Standards: Simple Yet Secure Authentication in Legacy Systems

by Zaid Fattuhi, 15/02/2022

 

Single Sign-On is a solved problem, right? There are even standards! SAML, OAuth 2.0, SCIM, OpenID Connect… So many standards. But what if you’re working with a legacy application that doesn’t support any of these? Well, that’s a situation we found ourselves in. I want to share how we overcame the challenge and what we learned along the way.

First, a little context.

We needed to be able to include UI elements from a newly developed web application (Spring Boot, ReactJS, RESTful APIs) into a legacy content management system (CMS) via <iframe> embeds. We also needed to provide a seamless experience for users so that they don’t feel that they’re moving between the two systems.

No problem, we said. Let’s use one of the standards-based SSO mechanisms.

Hold On

Not so fast. The legacy CMS doesn’t support modern SSO mechanisms. How do we solve the challenge of fulfilling the authentication requirements when we can’t use a standard approach to SSO?

We immediately identified two options.

Option 1: Shared Session

  1. Pass the sessionId in the browser from a CMS page to the new system via <iframe> URL
  2. Create a session validation service in the CMS to provide the new system a mechanism to validate any sessionIds it receives
  3. Update the new system so that it calls the session validation service on request to validate the received sessionId

We discounted this option as it introduces tighter coupling between the two systems. It could also lead to performance issues, as the session would potentially need to be validated on each request to the new system.

Option 2: Shared Credentials

  1. Pass the user credentials to the new system via <iframe> URL
  2. New system validates the credentials either by the authentication service in the CMS, or by directly accessing the database of the old system to verify the credentials

Again, we discounted this option because sharing credentials, even encrypted credentials, is poor practice, and accessing the CMS database from the new system introduces tighter coupling.

At that point, we decided to see if JWT might be a solution.

JWT to the Rescue

After a little exploration, we settled on the following approach:

  1. CMS system generates a JWT token (using a secret that it and the new system know) and passes that token to the new system in each <iframe> URL
  2. New system receives the JWT token, stores it securely in the browser (using, for example, HttpOnly cookie), which then uses the token to subsequent REST API calls.
  3. New system can validate JWT tokens because it knows the token secret. This ensures tokens are valid, authentic, and have not been tampered with.

Success! This approach worked out great – authenticated users were able to seamlessly and securely access both CMS content as well as content from the new system.

JWT gave us:

  1. Portability: JWT token carries its own unit of identity, and can be validated without having to call a 3rd party system (no performance impact!)
  2. Decentralisation: Both the JWT token issuer and consumer can be completely separate systems, as long as they share the same secrets. In our case the CMS is the JWT issuer, and the new system is the JWT consumer.
  3. Extensibility: Since the JWT token is not tightly coupled to any system, if a new system is to be integrated with the CMS in the future then the same JWT token can be used for this integration.
  4. Self contained: JWT tokens have sufficient information to identify the caller.

In addition to the above, JWT tokens are currently the de-facto standard mechanism for authentication, and following the standards is a great way of reducing the possibility of introducing yet another legacy system to the world.

Some Code

The following code snippets show how we achieved our goal.

CMS

In the CMS, the following code generates the JWT token. It’s in a JSP (did I mention that the CMS was legacy?).

<%@ page import="com.auth0.jwt.JWT,com.auth0.jwt.algorithms.Algorithm" %>

<% 
       Algorithm algorithm = Algorithm.HMAC256("secret value");
        String jwtToken = JWT.create()
                .withIssuer("auth0")
                .withExpiresAt(expiryDate)
                .sign(algorithm);

%>
<iframe src="....?token=<%=jwtToken %>">

When the page is rendered, the JWT token is generated and passed as a URL parameter in any iframe links.

New System

We introduced two classes to support JWT validation.

JWTTokenAuthRequestFilter is a Spring HTTP filter class. It ensures the JWT token is valid and has not expired.

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.exceptions.JWTVerificationException;
import org.springframework.web.filter.OncePerRequestFilter;


public class JWTTokenAuthRequestFilter extends OncePerRequestFilter {
private final JWTVerifier verifier;

public JWTTokenAuthRequestFilter(ApiProperties apiProperties) {
   Algorithm algorithm = Algorithm.HMAC256(apiProperties.getJwtSecret());
   verifier = JWT.require(algorithm)
           .build();
}

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

// Obtain jwt token from the http request
String authToken = ....

try {
   verifier.verify(authToken);
   filterChain.doFilter(request, response);
} catch (JWTVerificationException jwtVerificationException) {
   logger.info("Error while verifying the JWT token:", jwtVerificationException);
   response.sendError(HttpStatus.FORBIDDEN.value(), String.format("Invalid JWT Token, error: %s", jwtVerificationException.getMessage()));
   return;
}

...

}

JWTWebSecurityConfigurer registers the request filter (JWTTokenAuthRequestFilter above) in the Spring Context and handles requests as soon as they are received.

@Configuration
@EnableWebSecurity
public class JWTWebSecurityConfigurer extends WebSecurityConfigurerAdapter {
   private final JWTTokenAuthRequestFilter jwtTokenAuthRequestFilter;

   public JWTWebSecurityConfigurer(JWTTokenAuthRequestFilter jwtTokenAuthRequestFilter) {
this.jwtTokenAuthRequestFilter = jwtTokenAuthRequestFilter;
   }


@Override
protected void configure(HttpSecurity http) throws Exception {
   http.addFilterBefore(jwtTokenAuthRequestFilter, UsernamePasswordAuthenticationFilter.class)
           .addFilter(jwtTokenAuthRequestFilter)
           .authorizeRequests().anyRequest().permitAll();
}
...
}

 

Conclusion

We were really happy with this approach as it met our requirements without making compromises on security, and required minimal code change to implement. On top of that, should we need to integrate other applications with the CMS in the future, we’ll be able to use the same JWT token.

If you hit a challenge like this in the future, be sure to take advantage of JWT if you can!