Authentification et autorisation

Voici un focus sur le système d’autorisation développé à partir de SecureSocial, pour le CFP de Devoxx France et Devoxx BE. Ce code est prévu pour Play 2.2, et particulièrement le nouveau système d’ActionBuilder.

Cet article vous propose de couvrir un cas courant dans le développement d’une application Web : l’autorisation et l’authentification de vos utilisateurs. Comment donner accès à une partie de votre application Web, selon le profil de l’utilisateur ?

Voyons tout d’abord comment cela fonctionne. Avec Play, il est facile de composer des actions, afin de définir une sémantique simple pour votre sécurité. Contrairement à une approche avec des annotations « à la Java », nous avons ici un typage statique qui valide à la compilation, ce qui est très pratique, et pas plus verbeux que du Java.

La page d’accueil du Backoffice a besoin d’être sécurisée. Seul un utilisateur authentifié, et du groupe « administrator » doit avoir accès à celle-ci.

Comment faire cela avec Play 2.2 ? L’approche que j’utilise est simple : j’ai définis sous forme de trait, un super controller pour gérer l’authentification et l’autorisation. Lorsque je souhaite protéger l’accès à une action, afin que seul un utilisateur du group administrator y ait accès, voici comment je définis cela :

object Backoffice extends SecureCFPController {
  
  // Affiche la page d'accueil si l'utilisateur est authentifié, et qu'il appartient au groupe administrator
  def homeBackoffice() = SecuredAction(IsMemberOf("administrator")) {
    implicit request =>
      Ok(views.html.Backoffice.homeBackoffice())
  }

// .. some other action
}
  • homeBackoffice est une action, un point d’entrée de mon application Web.
  • SecuredAction vérifie si l’utilisateur est authentifié ou non, et le redirige vers une page d’authentification
  • IsMemberOf s’assure que l’utilisateur courant appartient au groupe « administrator »
  • Enfin la page d’accueil du backoffice sera affichée, si ces conditions sont respectées

Notez la composition et l’approche utilisée ici. Pas d’annotations en Scala, contrairement à l’approche Java. C’est mieux non ?

Mais là où cela devient plus compliqué, c’est lorsque vous souhaitez utiliser une Action asynchrone. La fonction doApprove dans l’exemple ci-dessous, effectue quelques opérations bloquantes. Pour éviter de bloquer les Threads principales de mon application Web, je décide d’exécuter donc dans un bloc asynchrone la partie métier. Notez qu’asynchrone ne veut pas dire « non bloquant », c’est parfois mal compris lorsque l’on utilise Play. Asynchrone signifie que je dédie ce bloc à une autre pool de Thread, qui peut d’ailleurs se configurer simplement dans la configuration de Play. En fait, tout est asynchrone lorsque l’on regarde Play2. Node.JS a exactement la même approche. La grosse différence, c’est que Java/Scala tourne sur plusieurs core, ce que ne fait pas V8, le moteur JS de Google, utilisé par Node.JS. Et oui… node.js n’est pas multi-coeur bien qu’il soit multi-threadé aussi.

Voici donc un autre exemple, plus compliqué, utilisé sur une action async :

  def doApprove(proposalId: String) = SecuredAction(IsMemberOf("administrator")).async {
    implicit request: SecuredRequest[play.api.mvc.AnyContent] =>
        // Some operation that might block
        //...
        // ...
        // Redirect when done
        Future.successful(Redirect(routes.CFPAdmin.home()))
      }
  }

Notez que j’utilise la fonction async de mon Action, nous allons voir plus loin comment implémenter SecuredAction, afin qu’il supporte async.

L’une des clés du système est de proposer un moyen pour authentifier et autoriser les utilisateurs, selon leur profil. Typiquement, un speaker ne doit pas pouvoir modifier le sujet de son voisin, voir les votes, ou tout autre idée originale du même genre.

Pour cela, j’ai préféré implémenter un système en partant de ce qui est fait dans SecureSocial, mais en plus simple. J’ai aussi câblé le tout pour utiliser  le principe des ActionBuilder de Play 2.2. Si vous n’êtes pas familier avec cette partie de Play, je vous encourage plutôt à utiliser SecureSocial, pour découvrir et apprendre ces principes. C’est compliqué.

Tout d’abord, je définis un trait, SecureCFPController comme suit :

trait SecureCFPController extends Controller {

  /**
   * A secured action.  If there is no user in the session the request is redirected
   * to the login page
   */
  object SecuredAction extends SecuredActionBuilder[SecuredRequest[_]] {
    /**
     * Creates a secured action
     */
    def apply[A]() = new SecuredActionBuilder[A](None)

    /**
     * Creates a secured action
     * @param authorize an Authorize object that checks if the user is authorized to invoke the action
     */
    def apply[A](authorize: Authorization) = new SecuredActionBuilder[A](Some(authorize))
  }

Voyons maintenant comment définir SecuredActionBuilder.

/**
   * A builder for secured actions
   *
   * @param authorize an Authorize object that checks if the user is authorized to invoke the action
   * @tparam A
   */
  class SecuredActionBuilder[A](authorize: Option[Authorization] = None) extends ActionBuilder[({type R[A] = SecuredRequest[A]})#R] {

    def invokeSecuredBlock[A](authorize: Option[Authorization],
                              request: Request[A],
                              block: SecuredRequest[A] => Future[SimpleResult]): Future[SimpleResult] = {
      implicit val req = request
      val result = for (
        authenticator -> SecureCFPController.findAuthenticator; // recherche si le user est logué
        user -> SecureCFPController.lookupWebuser(authenticator) // verifie et charge le Webuser
      ) yield {
        if (authorize.isEmpty || authorize.get.isAuthorized(user)) {
          block(SecuredRequest(user, request))
        } else {
          Future.successful {
            Redirect(routes.Application.index()).flashing("error" -> "Not Authorized")
          }
        }
      }

      result.getOrElse({
        val response = {
          Redirect(routes.Application.home()).flashing("error" -> Messages("Cannot access this resource, your profile does not belong to this security group"))
        }
        Future.successful(response)
      })
    }

    def invokeBlock[A](request: Request[A], block: SecuredRequest[A] => Future[SimpleResult]) =
      invokeSecuredBlock(authorize, request, block)
  }

A la ligne 9, je définis un objet Authorization, qui me permettra de définir les 2 types d’autorisation nécessaires pour mon application. Est-ce que j’ai accès ? Est-ce que j’appartiens à ce groupe de sécurité ?

Webuser est mon entité de base, qui représente un utilisateur au sens technique. On ne parle pas de speaker ou d’administrateur, mais bien d’un utilisateur authentifié, qui utilise le site.

...
...
/**
 *  Defines an Authorization for the CFP Webuser
 */
trait Authorization {
  def isAuthorized(webuser: Webuser): Boolean
}

/**
 *  Checks if user is member of a security group
 */
case class IsMemberOf(securityGroup: String) extends Authorization {
  def isAuthorized(webuser: Webuser): Boolean = {
    Webuser.isMember(webuser.uuid, securityGroup)
  }
}

/**
 * Check if a user belongs to one of the specified groups.
 */
case class IsMemberOfGroups(groups: List[String]) extends Authorization {
  def isAuthorized(webuser: Webuser): Boolean = {
    groups.exists(securityGroup => Webuser.isMember(webuser.uuid, securityGroup))
  }
}
... 

Voici le fichier complet sous forme de Gist, pour vous permettre de voir l’ensemble du code :

Avec ceci, nous avons maintenant tout ce qu’il faut pour sécuriser et gérer les autorisations par groupe d’utilisateur.

Ce code n’est pas très compliqué, mais surtout, il est très puissant. Il permet ensuite de n’avoir que quelques lignes dans le reste de l’application. Les contrôleurs sont sécurisés en récupérant le trait « SecureCFPController ». Chaque Action peut ensuite sélectionner la sémantique de sécurisation qu’elle souhaite. Je peux enfin utiliser la fonction async de Play tout en étant capable de gérer l’autorisation d’accès.

Pour récupérer l’utilisateur authentifié, je peux soit appeler la fonction currentUser, soit simplement m’appuyer sur la case class « SecuredRequest », sur laquelle je stocke mon Webuser.

// Dans SecureCFPController
case class SecuredRequest[A](webuser: Webuser, request: Request[A]) extends WrappedRequest(request)

// Dans un des controllers, dans une action 

object Backoffice extends SecureCFPController {

  def showWhoIsLogged = SecuredAction(IsMemberOf("cfp")) {
    implicit request: SecuredRequest[play.api.mvc.AnyContent] =>

      val currentWebuser = request.webuser

      val name = currentWebuser.cleanName

      Ok(s"Hello, you are $name")
  }

}

C’est tout pour ce qui est de la gestion de l’autorisation d’accès aux ressources.

Une pensée sur « Play 2.2 authorization, actionbuilder, async… »

  1. Merci beaucoup Nicolas,

    C’est exactement la doc qui me manquait pour faire les choses vraiment proprement.

Les commentaires sont fermés.