La backpressure du pauvre avec Akka et les futures

Leave a Comment

Il m’est souvent arrivé d’utiliser des acteurs Akka pour interroger des services tiers comme un index Elasticsearch ou une base de données. Je me suis trouvé dans des situations où le service est submergé par les requêtes de l’acteur consommateur. La solution facile consiste à bloquer sur chaque appel au service et à lui envoyer une nouvelle requête seulement s’il a répondu à la précédente. Soit la méthode pour évoquer le service :

 
def callService: Future[Response] = ???

Dans le code cela se traduit comme suit :

 
import scala.concurrent.{ Await, Future }
import scala.concurrent.duration._

val response = Await.result(callService, 3 seconds) 

Afin de comprendre la solution non bloquante que je vais vous présenter prenons cet exemple. Nous avons un service exposant une API pour récupérer des données en spécifiant un index de départ et le nombre d’entrées que l’on souhaite récupérer. La contrainte est que si on demande un nombre trop important d’entrées d’un coup le service peut se casser la gueule ! De même il peut s’écrouler et ou avoir des temps de réponse trop élevés s’il reçoit trop de requêtes simultanément.

La solution consiste en un système d’acteurs constitué d’un master et de plusieurs workers. Le master crée un ensemble de tâches qu’il fait exécuter par les workers. Une tâche est représentée par la structure suivante :

 
case class Task(from: Int, size: Int)

Au démarrage de l’application le master crée les tâches à exécuter :

 
class Master extends Actor with ActorLogging {
  var tasks = Tasks.empty
  def receive: Receive = {
    case message @ FetchData(from, to) =>
      tasks = Tasks.from(from, to, step)
      context.become(working)
      createAndStartWorkers()
  }
}

J’ai créé une petite structure, Tasks, afin de simplifier la gestion des tâches :

 
case class Tasks(values: Vector[Task]) {
  def isDone = values.isEmpty

  def next: Option[(Task, Tasks)] =
    if(isDone) None
    else Option((values.head, copy(values = values.drop(1))))
}

A la réception d’un message de type Task le worker récupère les données correspondantes :

 
class Worker(master: ActorRef) extends Actor {
  def receive: Receive = {
    case task @ Task(from, size) =>
      fetchData(from, size) onSuccess {
        case data =>  master ! GetTask
      }
  }

  def fetchData(from: Int, size: Int): Future[Data] = ???
}

Une fois que le worker finit de traiter une tâche il notifie le master qui lui assigne une nouvelle tâche ou arrête si tous les workers ont fini de bosser :

 
class Master extends Actor with ActorLogging {
  var tasks = Tasks.empty
  val step = 10
  val numberOfWorkers = 2
  var remainingWorkingWorkers = numberOfWorkers

  def waiting: Receive = {
    case message @ FetchData(from, to) =>
      tasks = Tasks.from(from, to, step)
      context.become(working)
      createAndStartWorkers()
  }

  def receive = waiting

  def working: Receive = {
    case GetTask =>
      tasks.next.fold({
        log.info("No more tasks... ;-)")
        remainingWorkingWorkers = remainingWorkingWorkers - 1
        shutdownIfAllTasksCompleted()
      }) {
        case (task, newTasks) =>
          tasks = newTasks
          sender ! task
      }
  }

  def createAndStartWorkers(): Unit = {
    (0 until numberOfWorkers) foreach(_ => context.actorOf(Props(new Worker(self))) ! StartWorking)
  }

  def allTasksCompleted = remainingWorkingWorkers == 0

  def shutdownIfAllTasksCompleted() =
    if(allTasksCompleted) context.system.shutdown()
}

Tout le code est disponible sur Github : https://github.com/nouhoum/akka-stuff/tree/poor-man-backpressure-akka.

Sécuriser son API HTTP (2) (Basic | Digest) Authentication

Leave a Comment

Après avoir posé les fondamentaux dans le précédent article nous allons dans celui-ci nous intéresser à deux méthodes d’authentification à savoir l’authentification basique (HTTP Basic Authentication) et l’authentification “Digest” qui sont assez simples à mettre en oeuvre.

L’authentification basique

L’authentification basique est similaire à ce que font la plupart des sites web lorsqu’ils permettent à leurs utilisateurs de s’authentifier via un formulaire qui récupèrent le couple login/mot de passe. La différence est que dans le cas de l’authentification basique les crédentials sont renseignés dans l’en-tête de la requête HTTP qui est envoyée au serveur donc à votre API. L’en-tête de la requête est formaté de la façon suivante :

Authorization: Basic Base64Encode(login:password)
Pour le couple johndoe:jane doe cela donne :
Authorization: Basic am9obmRvZTpqYW5lIGRvZQ==

En général vous n’avez pas besoin d’encoder à la main l’en-tête de la requête car c’est pris en charge par le client HTPP. Dans l’exemple suivant curl construit le bon en-tête :

curl -i -u johndoe:jane https://api.example.com 

L’encodage en base 64 n’offre aucune sécurité en soi car il suffit de faire l’opération inverse (Base64Decode) pour retrouver les infos en clair. En outre les credentials de l’utilisateur sont envoyés dans chaque requête avec l’authentification basique. Cela accroît le risque d’interception de ces informations sensibles.

Comment améliorer la sécurité de l’authentification basique ? Utiliser HTTPs, veiller à la sécurité du stockage des mots de passe et à la façon dont ils sont récupérés lors de l’authentification. Hacher les mots de passe avant de les stocker permet de faire en sorte qu’ils ne soient connus que des utilisateurs et les protège lorsqu’un attaquant accède au stockage car il ne verra que les haches et non les mots de passe en clair. Cependant pour que tout cela soit efficace vous devez choisir un algorithme de hachage qui ne permette pas de retrouver la donnée initiale à partir du hache. Le hachage des mots de passe est un vaste sujet qui vaut bien un article à lui tout seul.

L’authentification “digest”

L’authentification “digest” a été mise en place pour pallier les limitations de l’authentification basique. Elle n’envoie pas le mot de passe en clair dans la requête mais procède à l’authentification par un échange de challenge/réponse.

Le challenge est envoyé par le serveur en réponse à l’initiation de l’échange par le client. Il contient notamment un “nonce” unique à chaque processus d’authentification Digest et un code qui porte le petit d’”opaque” et doit être retourné au serveur dans la réponse au challenge. Dans sa réponse l’utilisateur spécifie son identité (le paramètre username) et la réponse au challenge posé par le serveur. Cette réponse contient un certain nombre d’informations dont un hache calculé avec l'algorithme MD5 come : Hache = MD5(username:password:realm) Ce hache et le login (username) permettent au serveur d’authentifier l’utilisateur. Cela nécessite cependant que le serveur stocke le hache MD5(username:password:realm) ! Cette page wiki donne plus de détails sur les échanges entre le serveur et le client. Vous pouvez initier une authentification digest avec curl comme suit :

curl -k –-digest –u username:password -v https://api.example.com

Pour la route

L’authentification basique est simple à mettre oeuvre mais présente toutefois l’inconvénient de transférer les mots de passe en clair. Il est sage de n’utiliser qu’avec TLS (a.k.a HTTPs) qui ajoute la confidentielle au niveau transport. En revanche l’authentification digest est un peu plus complexe à implémenter mais a le mérite de ne pas transférer les mots de passe en clair. Disposant de ces infos et de vos contraintes propres (deadline, sensibilité de votre API etc.) vous êtes suffisamment armé pour choisir entre ces deux schémas d'authentification.

Sécuriser son API HTTP (1)

Leave a Comment

Nous vivons une époque où les API web sont légion et les organisations qu’elles soient des “géants du web” ou non en développent pour être utilisées à la fois par leurs propres applications et par celles de tiers. Dans ce contexte de généralisation et d’ouverture des API la question de la sécurité se pose assez rapidement.

La bonne nouvelle est que les problèmes de sécurité que nous rencontrons sont assez classiques et ont déjà été, dans leur grande majorité, résolus par d’autres personnes. La mauvaise, eh oui il en a une, est que les solutions et standards sont tellement nombreux (OAuth, TLS, JWS, JWE, OpenID...) qu’on y perd rapidement !

En ce moment j’explore ces solutions et standards et compte partager ici mes notes en espérant qu’elles aideront les plus pressés d’entre vous.

Dans ce premier article je parle brièvement de quelques principes fondamentaux de la sécurité à savoir la confidentialité, l’intégrité, la disponibilité, la non répudiation, l’authentification, l’autorisation et l’audit.

Sans faire trop de chichis rentrons dans le vif du sujet.

La confidentialité

La confidentialité consiste à protéger les données de sorte qu’elles ne soient intelligibles qu’aux destinataires. Dans le cas qui m’intéresse le cas des APIs web la confidentialité peut être obtenue grâce au TLS (a.k.a HTTPs) qui ajoute une protection à la couche de transport du protocole HTTP. De nombreux sites utilisent HTTPs sur l’ensemble de leurs pages et non seulement sur les pages où transitent des informations sensibles comme les mots de passe ou des transactions financières.

L’intégration

L’intégrité consiste à s’assurer que les données n’ont pas été altérées au cours de leur transfert. En général on fait cette vérification avec un code d'authentification des données (MAC). Le protocole TLS, donc HTTPs, permet aussi la vérification de l’intégration des données !

La disponibilité

Toute API a une certaine utilité et s’adresse à un public donné. Elle doit être conçue de sorte qu’elle soit toujours disponible pour ses utilisateurs. La sécurité de l'API consiste aussi à fournir à cette garantie.

La non répudiation

Super importante surtout quand vous faites des transactions business (paiement, consommation de crédit, contraintes légales de traçabilité etc.) avec votre API, la non répudiation permet d’assurer qu’une entité ne peut rejeter (ou répudier) une action qu’elle effectue sur le système. Par exemple une consommation de crédits par un client via l’API ne pourrait être rejetée plus tard. La non répudiation nécessite un tiers de confiance comme une autorité de certification qui procède à la vérification de la transaction. Dans la pratique l’entité qui initie la transaction signe les données et l’autre entité vérifie l’intégrité des données auprès du tiers de confiance avant d’exécuter la transaction.

L’authentification

L’authentification consiste à s’assurer qu’une entité (un utilisateur) est bien qui elle prétend être. En général cela s’accomplit de trois manières différentes : à partir de quelque chose qu'il connait, à partir de quelque qu'il détient et à partir de quelque chose qu'il est ! Voyons ce que signifie chacune de ces trois options.

  • S’authentifier à partir de quelque chose que l’on connaît : C’est peut-être la manière la plus répandue. Il s’agit en fait de demander à l’utilisateur de fournir une information qui permet de l’authentifier. C’est par exemple le cas quand on lui demande un mot de passe sur un site web ou un code PIN pour accéder à un téléphone.
  • S’authentifier à partir de quelque chose que l’on détient : Dans cette forme d’authentification on se sert de quelque chose dont dispose une entité pour l’authentifier. Les cartes à puce et les certificats numériques en sont deux exemples courants.
  • S’authentifier à partir de quelque chose que l’on est : C'est la forme la plus forte car elle se base sur des choses comme la reconnaissance faciale ou rétinienne, les empreintes digitales etc.

Certains système utilisent plus d’une manière (ou facteur) d’authentifier un utilisateur d’où le terme “authentification multi-facteur”.

L’autorisation

L’autorisation, qui sous-entend que l’utilisateur a déjà été authentifié, consiste à s’assurer qu’un utilisateur authentifié ne peut accéder qu’aux ressources ou actions qui lui sont autorisées au sein du système. Par exemple quand vous vous connectez à votre compte Google vous pouvez accéder à vos mails et vos docs mais pas à ceux d’un autre compte Google.

L’audit

Il est important que le système produise des logs qui seront utilisés plus tard pour des besoins d’audit, de détection d’intrusion, d’abus etc. Ces logs doivent également être protégés notamment pour qu’ils ne soient pas altérés par un attaquant qui souhaite dissimuler ses activités illicites :-)).

Pour la route

Voilà pour les concepts de base ! Dans le prochain article j’essaierai d’illustrer mes propos avec du code mais en attendant place à l’exploration...

© Nouhoum TRAORE.. Fourni par Blogger.