Authentification RESTful via Spring

262

Problème:
nous avons une API RESTful basée sur Spring MVC qui contient des informations sensibles. L'API doit être sécurisée, mais l'envoi des informations d'identification de l'utilisateur (combo utilisateur / passe) avec chaque demande n'est pas souhaitable. Conformément aux directives REST (et aux exigences métier internes), le serveur doit rester sans état. L'API sera consommée par un autre serveur dans une approche de type mashup.

Exigences:

  • Le client fait une demande à .../authenticate(URL non protégée) avec des informations d'identification; Le serveur renvoie un jeton sécurisé qui contient suffisamment d'informations pour que le serveur puisse valider les demandes futures et rester sans état. Il s'agirait probablement des mêmes informations que le jeton Remember-Me de Spring Security .

  • Le client effectue des requêtes ultérieures vers diverses URL (protégées), en ajoutant le jeton obtenu précédemment en tant que paramètre de requête (ou, de manière moins souhaitable, un en-tête de requête HTTP).

  • On ne peut pas s'attendre à ce que le client stocke des cookies.

  • Comme nous utilisons déjà Spring, la solution devrait utiliser Spring Security.

Nous nous sommes cogné la tête contre le mur en essayant de faire en sorte que cela fonctionne, alors j'espère que quelqu'un a déjà résolu ce problème.

Compte tenu du scénario ci-dessus, comment pourriez-vous résoudre ce besoin particulier?

Chris Cashwell
la source
49
Salut Chris, je ne suis pas sûr de passer ce jeton dans le paramètre de requête est la meilleure idée. Cela apparaîtra dans les journaux, indépendamment de HTTPS ou HTTP. Les en-têtes sont probablement plus sûrs. Juste FYI. Grande question cependant. +1
jmort253
1
Quelle est votre compréhension des apatrides? Votre condition de jeton entre en collision avec ma compréhension des apatrides. La réponse d'authentification Http me semble la seule implémentation sans état.
Markus Malkusch
9
@MarkusMalkusch sans état fait référence à la connaissance du serveur des communications antérieures avec un client donné. HTTP est sans état par définition, et les cookies de session le rendent avec état. La durée de vie (et la source, d'ailleurs) du jeton ne sont pas pertinentes; le serveur se soucie seulement qu'il est valide et peut être lié à un utilisateur (PAS à une session). La transmission d'un jeton d'identification n'interfère donc pas avec l'état.
Chris Cashwell du
1
@ChrisCashwell Comment vous assurez-vous que le jeton n'est pas usurpé / généré par le client? Utilisez-vous une clé privée côté serveur pour chiffrer le jeton, la fournir au client, puis utiliser la même clé pour le déchiffrer lors de futures demandes? Évidemment, Base64 ou un autre obscurcissement ne serait pas suffisant. Pouvez-vous élaborer sur les techniques de "validation" de ces jetons?
Craig Otis
6
Bien que cela soit daté et que je n'ai pas touché ou mis à jour le code depuis plus de 2 ans, j'ai créé un Gist pour développer davantage ces concepts. gist.github.com/ccashwell/dfc05dd8bd1a75d189d1
Chris Cashwell

Réponses:

190

Nous avons réussi à faire fonctionner cela exactement comme décrit dans le PO, et j'espère que quelqu'un d'autre pourra utiliser la solution. Voici ce que nous avons fait:

Configurez le contexte de sécurité comme suit:

<security:http realm="Protected API" use-expressions="true" auto-config="false" create-session="stateless" entry-point-ref="CustomAuthenticationEntryPoint">
    <security:custom-filter ref="authenticationTokenProcessingFilter" position="FORM_LOGIN_FILTER" />
    <security:intercept-url pattern="/authenticate" access="permitAll"/>
    <security:intercept-url pattern="/**" access="isAuthenticated()" />
</security:http>

<bean id="CustomAuthenticationEntryPoint"
    class="com.demo.api.support.spring.CustomAuthenticationEntryPoint" />

<bean id="authenticationTokenProcessingFilter"
    class="com.demo.api.support.spring.AuthenticationTokenProcessingFilter" >
    <constructor-arg ref="authenticationManager" />
</bean>

Comme vous pouvez le voir, nous avons créé une coutume AuthenticationEntryPoint, qui renvoie simplement un 401 Unauthorizedsi la demande n'a pas été authentifiée dans la chaîne de filtrage par notre AuthenticationTokenProcessingFilter.

CustomAuthenticationEntryPoint :

public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response,
            AuthenticationException authException) throws IOException, ServletException {
        response.sendError( HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized: Authentication token was either missing or invalid." );
    }
}

AuthenticationTokenProcessingFilter :

public class AuthenticationTokenProcessingFilter extends GenericFilterBean {

    @Autowired UserService userService;
    @Autowired TokenUtils tokenUtils;
    AuthenticationManager authManager;

    public AuthenticationTokenProcessingFilter(AuthenticationManager authManager) {
        this.authManager = authManager;
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
            FilterChain chain) throws IOException, ServletException {
        @SuppressWarnings("unchecked")
        Map<String, String[]> parms = request.getParameterMap();

        if(parms.containsKey("token")) {
            String token = parms.get("token")[0]; // grab the first "token" parameter

            // validate the token
            if (tokenUtils.validate(token)) {
                // determine the user based on the (already validated) token
                UserDetails userDetails = tokenUtils.getUserFromToken(token);
                // build an Authentication object with the user's info
                UsernamePasswordAuthenticationToken authentication = 
                        new UsernamePasswordAuthenticationToken(userDetails.getUsername(), userDetails.getPassword());
                authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails((HttpServletRequest) request));
                // set the authentication into the SecurityContext
                SecurityContextHolder.getContext().setAuthentication(authManager.authenticate(authentication));         
            }
        }
        // continue thru the filter chain
        chain.doFilter(request, response);
    }
}

De toute évidence, TokenUtilscontient du code privé (et très spécifique à la casse) et ne peut pas être facilement partagé. Voici son interface:

public interface TokenUtils {
    String getToken(UserDetails userDetails);
    String getToken(UserDetails userDetails, Long expiration);
    boolean validate(String token);
    UserDetails getUserFromToken(String token);
}

Cela devrait vous permettre de prendre un bon départ. Codage heureux. :)

Chris Cashwell
la source
Est-il nécessaire d'authentifier le jeton lors de l'envoi du jeton avec la demande. Que diriez-vous d'obtenir les informations sur le nom d'utilisateur directement et de les définir dans le contexte / la demande actuelle?
Fisher
1
@Spring Je ne les stocke nulle part ... l'idée du jeton est qu'il doit être transmis à chaque demande, et il peut être déconstruit (partiellement) pour déterminer sa validité (d'où la validate(...)méthode). Ceci est important car je veux que le serveur reste sans état. J'imagine que vous pourriez utiliser cette approche sans avoir besoin d'utiliser Spring.
Chris Cashwell
1
Si le client est un navigateur, comment stocker le jeton? ou devez-vous refaire l'authentification pour chaque demande?
beginner_
2
bons conseils. @ChrisCashwell - la partie que je ne trouve pas est où validez-vous les informations d'identification de l'utilisateur et renvoyez-vous un jeton? Je suppose que cela devrait être quelque part dans l'impl du point de terminaison / authenticate. ai-je raison ? Si non, quel est l'objectif de / authentifier?
Yonatan Maman
3
qu'y a-t-il dans AuthenticationManager?
MoienGK
25

Vous pourriez envisager l' authentification Digest Access . Le protocole est essentiellement le suivant:

  1. La demande est faite du client
  2. Le serveur répond avec une chaîne nonce unique
  3. Le client fournit un nom d'utilisateur et un mot de passe (et certaines autres valeurs) md5 haché avec le nonce; ce hachage est appelé HA1
  4. Le serveur est alors en mesure de vérifier l'identité du client et de fournir le matériel demandé
  5. La communication avec le nonce peut continuer jusqu'à ce que le serveur fournisse un nouveau nonce (un compteur est utilisé pour éliminer les attaques de relecture)

Toutes ces communications sont effectuées via des en-têtes, qui, comme le souligne jmort253, sont généralement plus sûrs que la communication de documents sensibles dans les paramètres d'URL.

L'authentification Digest Access est prise en charge par Spring Security . Notez que, bien que les documents disent que vous devez avoir accès au mot de passe en texte brut de votre client, vous pouvez vous authentifier avec succès si vous disposez du hachage HA1 pour votre client.

Tim Pote
la source
1
Bien qu'il s'agisse d'une approche possible, les nombreux allers-retours qui doivent être effectués pour récupérer un jeton le rendent un peu indésirable.
Chris Cashwell
Si votre client suit la spécification d'authentification HTTP, ces allers-retours se produisent uniquement lors du premier appel et lorsque cela se produit.
Markus Malkusch
5

En ce qui concerne les jetons transportant des informations, les jetons Web JSON ( http://jwt.io ) sont une technologie brillante. Le concept principal consiste à intégrer des éléments d'information (revendications) dans le jeton, puis à signer l'ensemble du jeton afin que l'extrémité de validation puisse vérifier que les revendications sont bien fiables.

J'utilise cette implémentation Java: https://bitbucket.org/b_c/jose4j/wiki/Home

Il existe également un module Spring (spring-security-jwt), mais je n'ai pas examiné ce qu'il prend en charge.

Leif John
la source