Sécuriser ses services REST



Développer son application via des API c’est extremement pratique et évolutif, certes, mais cela rend plus complexe la sécurisation des applications sous-jacentes. Le Framework JOY est maintenant opérationel pour faire du CRUD … il me reste maintenant à mettre en place les mécanismes de sécurité adéquates. Alors évidemment, je me suis renseigné sur les mécanismes oAuth 1 et 2 qui sont les plus utilisés, même si très honnetement ils ont plus été conçus (surtout la v2) pour des applications B2C (via l’utilisation de réseaux sociaux).

Insipiré de tout celà j’ai décidé d’implémenter mon propre mécanique directement dansle framework serveur mais aussi de fournir le framework client (Javascript) qui fonctionne avec.

Les principes d’authentification

Quant on développait des application web à la mode JSP on utilisait des sessions qui en fait utilisaient la plupart du temps des token (transmis bien souvent via des cookies). Ces token étaient gérés par J2EE ce qui fait qu’on ne s’en occupait guère … on se contentait de récupérer l’objet session et de travailler avec. Ce temps là est bien révolu et nous alllons en quelque sorte devoir re produire ce type de mécanisme dans notre API.

Tout d’abord nous utiliserons l’entête HTTP « Authentication », c’est à cet endroit en effet qu’il est prévu d’y envoyer le token de session:

String headerAuth = _request.getHeader("Authorization");

A chaque appel d’API le framework récupérera ce token pour le contrôler.

Mais celà veut dire que l’application doit maintenir ce token pendant son utilisation. Et bien sur, la solution la plus simple pour maintenir une donnée coté client est l’utilisation de cookies. Nous créerons donc coté framework client la possibilité de naviguer à travers les écrans en affectant et récupérant les cookies.

Le token

voici à quoi ressemble le token de session dans le framework Joy :

Ce dernier est composé de deux parties :

  1. La première contient la clé publique (non cryptée)
  2. La seconde contient la clé publique + une date & heure. Ces deux données étant cryptées via une clé privée que seule l’application connait.

On utilise le carractère | comme délimiteur.

Pourquoi une date en plus me direz-vous ?

Pour deux raisons en fait. La première est pour éviter que le cryptage de la clé publique soit toujours le même et ainsi complexifier un piratage de type « man in the middle ». La seconde raison est de rajouter un autre controle (autre que celui de vérification des clés publiques)  de type vérification timeout. Selon la configuration de l’application appelante on pourra considérer facilement que si le token n’a pas été utilisé pendant 10 minutes, un login est de nouveau nécéssaire.

Attention, car rajouter ce controle de timeout implique aussi de recalculer le token à chaque appel !

Coté framework serveur

2 cas de figures sont à envisager :

  1. La phase de login elle même qui permet la création du token
  2. Les phases d’appels d’API dans lesquelles on vérifiera la validité du token

Le login

Le login sera donc effectué dans Joy via un nouveau filtre java http : FilterAuthenticate.

Pendant cette phase de login, le controle login/mot de passe sera bien entendu effectué et un token sera calculé et renvoyé automatiquement. Afin de prendre en compte les spécificités de l’application le filtre doit être hérité de la sorte :

public class dgmAuthFilter extends FilterAuthenticate {
    @Override
    protected String getPublicKey(JoyApiRequest request) {
        return request.getParameter(C.REQ_PARAM_USER_TAG).getValue();
    }
    
    @Override
    protected boolean checkLogin(JoyApiRequest request) {
        return (request.getParameter(C.REQ_PARAM_USER_TAG).getValue() != null && request.getParameter(C.REQ_PARAM_PWD_TAG).getValue() != null);
    }  
}

Les appels d’API

Le framework Joy contrôle ensuite automatiquement les appels d’API dans le filtre http : FilterCommon.

La méthode checkToken peut d’ailleurs être surchargée (facultatif) pour changer le contrôle :

    protected boolean checkToken(JoyState state) {
        try {
            JoyAuthToken myCookie = new JoyAuthToken();
            myCookie.setPrivateKey(state.getAppParameters().getAuthPrivateKey());
            myCookie.setTimeoutInMinutes(state.getAppParameters().getSessionItemout());
            return myCookie.checkAuthCookie(state.getHttpAuthToken());
            
        } catch (Exception ex) {
            this.getLog().log(Level.WARNING, "checkToken|Exception> {0}", ex.toString());
            return false;
        }
    }

Pour faire simple le framework fournit une classe JoyAuthToken qui gère le token entièrement (avec l’encryption).

L’encryption

La classe JoyAuthToken fournit 3 méthodes d’encryptage/décryptage que j’ai d’ailleurs récupéré sur internet :

    private Key generateKey() throws Exception {
        byte[] keyValue = this.privateKey.getBytes("UTF-8");
        MessageDigest sha = MessageDigest.getInstance("SHA-1");
        keyValue = sha.digest(keyValue);
        keyValue = Arrays.copyOf(keyValue, 16); // use only first 128 bit       
        Key key = new SecretKeySpec(keyValue, C.AUTH_ALGO);
        return key;
    }

    protected String encrypt(String Data) throws Exception {
        Key key = generateKey();
        Cipher c = Cipher.getInstance(C.AUTH_ALGO);
        c.init(Cipher.ENCRYPT_MODE, key);
        byte[] encVal = c.doFinal(Data.getBytes());
        String encryptedValue = DatatypeConverter.printBase64Binary(encVal);
        return encryptedValue;
    }

    protected String decrypt(String encryptedData) throws Exception {
        Key key = generateKey();
        Cipher c = Cipher.getInstance(C.AUTH_ALGO);
        c.init(Cipher.DECRYPT_MODE, key);       
        byte[] decordedValue = DatatypeConverter.parseBase64Binary(encryptedData);
        byte[] decValue = c.doFinal(decordedValue);
        String decryptedValue = new String(decValue);
        return decryptedValue;
    }

Cette classe se base sur deux parametres qu’il faut modifier dans la configuration du framework :

  1. La clé privée : fichier joy-parameters.xml (tag <auth-private-key>)
  2. Le timeout : fichier joy-parameters.xml (tag <joy-session-timeout>)

Coté framework client

Coté client il faut que le framework puisse gérer :

  1. La création/suppression de cookies
  2. L’ajout du token lors d’appels Ajax

Pour le login

Voici un exemple de login avec appel de la classe d’authentification et retour et affectation du token à un cookie :

function cb_login_return(content) {
    if(content.status == 1) {
        $("#errormessages").hide();
        $$.setToken(content);
        $$.navigate("home");
    } else {
        $("#errormessages").show();
        $("#errormessages").html("Login failed, please retry");
    }
}

function login() {
    var postParams = {
        "user" : $("#joyuser").val(),
        "password" : $("#joypassword").val()
    };
    $$.ajax("POST", cb_login_return, $$.getLOGINCall(), postParams);
}

Dans cet exemple la page de login a deux INPUT (joyuser et joypassword). L’appel de la fonction setToken() permet elle de créer le cookie avec le token dedans.

Pour la navigation

Pour les appels Ajax

J’ai du modifier quelque peu la méthode d’appel de fonction ajax :

    this.ajax = function(m, cb, url, valObj) {
        // Get the session cookie
        var myCookie = this.getToken();
        if (myCookie == null) { 
            // NO SESSION ...
        }
        
        ...
	myxhr.onreadystatechange = function() {
            if(myxhr.readyState==4){
                switch(myxhr.status) {
                    case 200: cb(eval("(" + myxhr.responseText + ")")); break;
                    case 403, 404, 503 :  cb(null); break;
                    case 401: JOY.navigate("login"); break; // Redirect login page
                    default:  cb(null);	
		}
            }
	}
    };

Il fallait en effet rajouter deux choses :

  1. La récupération du token (cookie) et le placer automatiquement dans l’entête HTTP (Authentication)
  2. L’erreur de vérification du token et la redirection automatique (erreur 401).

Les cookies

J’ai aussi récupéré sur internet deux petites fonctions javascript pour gérer simplement les cookies :

    this.setCookie = function(nom, valeur, jours) {
        if (jours) {
            var date = new Date();
            date.setTime(date.getTime()+(jours*24*60*60*1000));
            var expire = "; expire="+date.toGMTString();
        }
        // Aucune valeur de jours spécifiée
        else var expire = "";
        document.cookie = nom+"="+valeur+expire+"; path=/";
    }

    this.getCookie = function(name) {
	var nameEQ = name + "=";
	var ca = document.cookie.split(';');
	for(var i=0;i < ca.length;i++) {
		var c = ca[i];
		while (c.charAt(0)==' ') c = c.substring(1,c.length);
		if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length,c.length);
	}
	return null;
    }
    
    this.delCookie = function(nom) {
        this.setCookie(nom, "", -1);
    }

Et voilà le framework (client + server) est totalement opérationnel 🙂

Récupérer le framework ici



A propos de Benoit Cayla

En plus d'être un spécialiste des données et processus d'entreprise, j'aime passer le plus clair de mon temps à l'extérieur, à jardiner par exemple ou simplement à aller dans le bois avec mes enfants. J'adore aussi le VTT que je pratique chaque semaine avec mes amis. Quand je dois rester à l'intérieur, j'aime améliorer ma maison. Bricoler me permet ainsi de libérer ma créativité d'ingénieur. Et, si je n'ai rien à réparer ou créer, je passe mon temps libre à explorer les dernières avancées technologiques dans le monde de l'IA et du RPA.

Laissez un commentaire

Votre adresse de messagerie ne sera pas publiée.

5 − 1 =

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.