POSTED ON 15 FEB 2022
READING TIME: 5 MINUTES
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.
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
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
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.
After a little exploration, we settled on the following approach:
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:
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.
The following code snippets show how we achieved our goal.
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.
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();
}
...
}
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!