27

I'm trying to manage sessions in Spring Security without leveraging cookies. The reasoning is - our application is displayed within an iframe from another domain, we need to manage sessions in our application, and Safari restricts cross-domain cookie creation. (context : domainA.com displays domainB.com in an iframe. domainB.com is setting a JSESSIONID cookie to leverage on domainB.com, but since the user's browser is showing domainA.com - Safari restricts domainB.com from creating the cookie).

The only way I can think to achieve this (against OWASP security recommendations) - is to include the JSESSIONID in the URL as a GET parameter. I don't WANT to do this, but I can't think of an alternative.

So this question is both about :

  • Are there better alternatives to tackling this problem?
  • If not - how can I achieve this with Spring Security

Reviewing Spring's Documentation around this, using enableSessionUrlRewriting should allow for this

So I've done this :

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        .sessionManagement()
            .sessionCreationPolicy(SessionCreationPolicy.ALWAYS)
            .enableSessionUrlRewriting(true)

This didn't add the JSESSIONID to the URL, but it should be allowed now. I then leveraged some code found in this question to set the "tracking mode" to URL

@SpringBootApplication
public class MyApplication extends SpringBootServletInitializer {

   @Override
   public void onStartup(ServletContext servletContext) throws ServletException {
      super.onStartup(servletContext);

      servletContext
        .setSessionTrackingModes(
            Collections.singleton(SessionTrackingMode.URL)
      );

Even after this - the application still adds the JSESSIONID as a cookie and not in the URL.

Can someone help point me in the right direction here?

Phas1c
  • 838
  • 2
  • 9
  • 21
  • You can solve it in an other way as well. If you have a SPA than you can utilize headers and memory. Also if you are using an `iframe` it should be OK to manage your own cookies, you should create a servlet that "shares" the session in the two domains or I think the best solution would be to have a proxy on domainA.com which points to domainB.com. This way you can set up any cookie mapping, anything what you want. If you can utilize this I'd use https://github.com/mitre/HTTP-Proxy-Servlet . I'll post this as a detailed answer if you are able to utilize any of these methods. :) – Hash Jun 08 '18 at 14:47
  • Try this and let me know. servletContext.setSessionTrackingModes(EnumSet.of(SessionTrackingMode.URL)); – Shubham Kadlag Jun 13 '18 at 08:47

4 Answers4

8

Have you looked at Spring Session: HttpSession & RestfulAPI which uses HTTP headers instead of cookies. See the REST sample projects in REST Sample.

Jean Marois
  • 1,362
  • 10
  • 17
  • I posted the solution I ended up going with (more infrastructural and not application-level changes) - but had I needed another solution - this would have been the runner up. Thanks! – Phas1c Jun 19 '18 at 04:56
  • This example works with authentication, but I need to get a session for every user (with or without authentication) like regular sessions works. Is that possible? – David Canós Dec 05 '18 at 00:25
  • @DavidCanós, it should. This approach only changes how the session information is transmitted between the client and server, i.e. in the header instead of a cookie or part of the url, but I haven't tried your use case. – Jean Marois Dec 05 '18 at 15:01
  • @JeanMarois i managed to do it. It just works as expected, you have to send x-auth-token in the header and it really works as a cookie (auth or not auth sessions). It creates a session in the server and allow you to work as usual. Thanks – David Canós Dec 07 '18 at 00:50
3

You can have a token based communication between the site DomainB.com server and the client browser. The token can be sent from the DomainB.com server in the response's header , after authentication. The client browser can then save the token in localstorage/session storage (have a expiry time too). The client can then send the token in every request's header. Hope this helps.

Amit Parashar
  • 1,354
  • 11
  • 15
3

Form based logins are mainly stateful sessions. In your scenario using stateless sessions would be best.

JWT provide implementation for this. Its basically a key which you need to pass as header in each HTTP request. So as long as you have the key. API is available.

We can integrate JWT with Spring.

Basically you need to write these logic.

  • Generate Key Logic
  • Use JWT in Spring Security
  • Validate key on each call

I can give you a head start

pom.xml

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.0</version>
</dependency>

TokenHelper.java

Contain useful functions for validating, checking, and parsing Token.

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import com.test.dfx.common.TimeProvider;
import com.test.dfx.model.LicenseDetail;
import com.test.dfx.model.User;


@Component
public class TokenHelper {

    protected final Log LOGGER = LogFactory.getLog(getClass());

    @Value("${app.name}")
    private String APP_NAME;

    @Value("${jwt.secret}")
    public String SECRET;    //  Secret key used to generate Key. Am getting it from propertyfile

    @Value("${jwt.expires_in}")
    private int EXPIRES_IN;  //  can specify time for token to expire. 

    @Value("${jwt.header}")
    private String AUTH_HEADER;


    @Autowired
    TimeProvider timeProvider;

    private SignatureAlgorithm SIGNATURE_ALGORITHM = SignatureAlgorithm.HS512;  // JWT Algorithm for encryption


    public Date getIssuedAtDateFromToken(String token) {
        Date issueAt;
        try {
            final Claims claims = this.getAllClaimsFromToken(token);
            issueAt = claims.getIssuedAt();
        } catch (Exception e) {
            LOGGER.error("Could not get IssuedDate from passed token");
            issueAt = null;
        }
        return issueAt;
    }

    public String getAudienceFromToken(String token) {
        String audience;
        try {
            final Claims claims = this.getAllClaimsFromToken(token);
            audience = claims.getAudience();
        } catch (Exception e) {
            LOGGER.error("Could not get Audience from passed token");
            audience = null;
        }
        return audience;
    }

    public String refreshToken(String token) {
        String refreshedToken;
        Date a = timeProvider.now();
        try {
            final Claims claims = this.getAllClaimsFromToken(token);
            claims.setIssuedAt(a);
            refreshedToken = Jwts.builder()
                .setClaims(claims)
                .setExpiration(generateExpirationDate())
                .signWith( SIGNATURE_ALGORITHM, SECRET )
                .compact();
        } catch (Exception e) {
            LOGGER.error("Could not generate Refresh Token from passed token");
            refreshedToken = null;
        }
        return refreshedToken;
    }

    public String generateToken(String username) {
        String audience = generateAudience();
        return Jwts.builder()
                .setIssuer( APP_NAME )
                .setSubject(username)
                .setAudience(audience)
                .setIssuedAt(timeProvider.now())
                .setExpiration(generateExpirationDate())
                .signWith( SIGNATURE_ALGORITHM, SECRET )
                .compact();
    }



    private Claims getAllClaimsFromToken(String token) {
        Claims claims;
        try {
            claims = Jwts.parser()
                    .setSigningKey(SECRET)
                    .parseClaimsJws(token)
                    .getBody();
        } catch (Exception e) {
            LOGGER.error("Could not get all claims Token from passed token");
            claims = null;
        }
        return claims;
    }

    private Date generateExpirationDate() {
        long expiresIn = EXPIRES_IN;
        return new Date(timeProvider.now().getTime() + expiresIn * 1000);
    }

    public int getExpiredIn() {
        return EXPIRES_IN;
    }

    public Boolean validateToken(String token, UserDetails userDetails) {
        User user = (User) userDetails;
        final String username = getUsernameFromToken(token);
        final Date created = getIssuedAtDateFromToken(token);
        return (
                username != null &&
                username.equals(userDetails.getUsername()) &&
                        !isCreatedBeforeLastPasswordReset(created, user.getLastPasswordResetDate())
        );
    }

    private Boolean isCreatedBeforeLastPasswordReset(Date created, Date lastPasswordReset) {
        return (lastPasswordReset != null && created.before(lastPasswordReset));
    }

    public String getToken( HttpServletRequest request ) {
        /**
         *  Getting the token from Authentication header
         *  e.g Bearer your_token
         */
        String authHeader = getAuthHeaderFromHeader( request );
        if ( authHeader != null && authHeader.startsWith("Bearer ")) {
            return authHeader.substring(7);
        }

        return null;
    }

    public String getAuthHeaderFromHeader( HttpServletRequest request ) {
        return request.getHeader(AUTH_HEADER);
    }


}

WebSecurity

SpringSecurity Logic to add JWT check

@Override
    protected void configure(HttpSecurity http) throws Exception {
        http
        .sessionManagement().sessionCreationPolicy( SessionCreationPolicy.STATELESS ).and()
        .exceptionHandling().authenticationEntryPoint( restAuthenticationEntryPoint ).and()
        .authorizeRequests()
        .antMatchers("/auth/**").permitAll()
        .antMatchers("/login").permitAll()
        .antMatchers("/home").permitAll()
        .antMatchers("/actuator/**").permitAll()
        .anyRequest().authenticated().and()
        .addFilterBefore(new TokenAuthenticationFilter(tokenHelper, jwtUserDetailsService), BasicAuthenticationFilter.class);

        http.csrf().disable();
    }

TokenAuthenticationFilter.java

Check each Rest Call for valid Token

package com.test.dfx.security;

import java.io.IOException;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.web.filter.OncePerRequestFilter;

public class TokenAuthenticationFilter extends OncePerRequestFilter {

    protected final Log logger = LogFactory.getLog(getClass());

    private TokenHelper tokenHelper;

    private UserDetailsService userDetailsService;

    public TokenAuthenticationFilter(TokenHelper tokenHelper, UserDetailsService userDetailsService) {
        this.tokenHelper = tokenHelper;
        this.userDetailsService = userDetailsService;
    }


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

        String username;
        String authToken = tokenHelper.getToken(request);

        logger.info("AuthToken: "+authToken);

        if (authToken != null) {
            // get username from token
            username = tokenHelper.getUsernameFromToken(authToken);
            logger.info("UserName: "+username);
            if (username != null) {
                // get user
                UserDetails userDetails = userDetailsService.loadUserByUsername(username);
                if (tokenHelper.validateToken(authToken, userDetails)) {
                    // create authentication
                    TokenBasedAuthentication authentication = new TokenBasedAuthentication(userDetails);
                    authentication.setToken(authToken);
                    SecurityContextHolder.getContext().setAuthentication(authentication);
                }
            }else{
                logger.error("Something is wrong with Token.");
            }
        }
        chain.doFilter(request, response);
    }


}
MyTwoCents
  • 6,028
  • 3
  • 18
  • 41
2

I appreciate all the answers above - I ended up opting for an easier solution without making any application-level changes because the owner of domainA.com was willing to work with us. Posting it here for others, as I didn't even think of this originally...

Basically :

  • Owner of domainA.com created a DNS record to for domainB.domainA.com -> domainB.com
  • Owner of domainB.com (me) requested a public SSL certificate for domainB.domainA.com via "email validation" (I did this through AWS, but I'm sure there are other mechanisms via othe providers)
  • The above request was sent to the webmasters of domainA.com -> they approved and issued the public certificate
  • Once issued - I was able to configure my application (or load balancer) to use this new certificate, and they configured their application to point to "domainB.domainA.com" (which subsequently routed to domainB.com in DNS)
  • Now, the browsers issue cookies for domainB.domainA.com and since they are the same primary domain, the cookies get created without any work-arounds needed.

Thanks again for the answers, apologies for not selecting an answer here - busy week.

Phas1c
  • 838
  • 2
  • 9
  • 21