web-dev-qa-db-fra.com

Sessions de sécurité de printemps sans cookies

J'essaie de gérer des sessions dans Spring Security sans exploiter les cookies. Le raisonnement est le suivant: notre application est affichée dans une iframe d'un autre domaine, nous devons gérer les sessions dans notre application, et Safari limite la création de cookies entre domaines . (contexte: domainA.com affiche domainB.com dans une iframe. domainB.com définit un cookie JSESSIONID à exploiter sur domainB.com, mais comme le navigateur de l'utilisateur affiche domainA.com - Safari empêche domainB.com de créer le cookie) .

La seule façon pour moi d'y parvenir (par rapport aux recommandations de sécurité OWASP) est d'inclure le JSESSIONID dans l'URL en tant que paramètre GET. Je ne veux pas faire cela, mais je ne peux pas penser à une alternative.

Donc, cette question concerne à la fois:

  • Existe-t-il de meilleures solutions pour résoudre ce problème?
  • Sinon, comment puis-je y parvenir avec Spring Security

Passer en revue la documentation de Spring à ce sujet en utilisant enableSessionUrlRewriting devrait permettre cela

Alors j'ai fait ça:

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

Cela n'a pas ajouté le JSESSIONID à l'URL, mais cela devrait être autorisé maintenant. J'ai ensuite exploité le code trouvé dans cette question pour définir le "mode de suivi" sur URL

@SpringBootApplication
public class MyApplication extends SpringBootServletInitializer {

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

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

Même après cela, l'application ajoute toujours le JSESSIONID en tant que cookie et non dans l'URL.

Quelqu'un peut m'aider à me diriger dans la bonne direction ici?

21
Phas1c

Avez-vous regardé Spring Session: HttpSession & RestfulAPI qui utilise des en-têtes HTTP au lieu de cookies. Voir les REST exemples de projets dans exemples REST .

5
Jean Marois

Les connexions basées sur des formulaires sont principalement des sessions avec état. Dans votre scénario, utiliser des sessions sans état serait préférable.

JWT fournit une implémentation pour cela. C'est essentiellement une clé que vous devez passer comme en-tête dans chaque requête HTTP. Donc, tant que vous avez la clé. L'API est disponible.

Nous pouvons intégrer JWT avec Spring.

Fondamentalement, vous devez écrire cette logique.

  • Générer une logique de clé
  • Utiliser JWT dans Spring Security
  • Valider la clé à chaque appel

Je peux vous donner une longueur d'avance

pom.xml

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

TokenHelper.Java

Contient des fonctions utiles pour la validation, la vérification et l'analyse de 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

Logique SpringSecurity à ajouter à la vérification JWT

@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

Vérifiez chaque appel restant pour un jeton valide

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);
    }


}
3
MyTwoCents

Vous pouvez établir une communication à base de jeton entre le serveur DomainB.com du site et le navigateur client. Le jeton peut être envoyé à partir du serveur DomainB.com dans l'en-tête de la réponse, après authentification. Le navigateur client peut alors enregistrer le jeton dans le stockage localstorage/session (également avec un délai d'expiration). Le client peut ensuite envoyer le jeton dans l'en-tête de chaque demande. J'espère que cela t'aides.

3
Amit Parashar

J'apprécie toutes les réponses ci-dessus - j'ai finalement opté pour une solution plus simple sans apporter de modifications au niveau de l'application, car le propriétaire de domainA.com était disposé à travailler avec nous. Le poster ici pour les autres, car je n'y avais même pas pensé à l'origine ...

Fondamentalement :

  • Le propriétaire de domainA.com a créé un enregistrement DNS pour domainB.domainA.com -> domainB.com
  • Le propriétaire de domainB.com (moi) a demandé un certificat SSL public pour domainB.domainA.com via "la validation du courrier électronique" (je l'ai fait via AWS, mais je suis sûr qu'il existe d'autres mécanismes via d'autres fournisseurs).
  • La demande ci-dessus a été envoyée aux webmasters de domainA.com -> ils ont approuvé et délivré le certificat public.
  • Une fois émise, j'ai pu configurer mon application (ou l'équilibreur de charge) pour utiliser ce nouveau certificat et ils ont configuré leur application pour pointer vers "domainB.domainA.com" (qui a ensuite été routé vers domainB.com dans DNS).
  • Désormais, les navigateurs émettent des cookies pour domainB.domainA.com et, comme il s’agit du même domaine principal, les cookies sont créés sans aucune solution de rechange.

Merci encore pour les réponses, excuses pour ne pas avoir choisi de réponse ici - semaine bien remplie.

0
Phas1c