dimanche 6 janvier 2013

AngularJS + Play! Framework: Authentification





En tant que newbie sur Angular, je me suis posé la question de l'authentification dans ce type d'applications.

Pour ceux qui veulent aller directement à la présentation du code, c'est
.



Depuis longtemps je suis habitué au développement de web apps dont les pages sont générées dynamiquement sur le serveur (JSP, JSF, PHP, etc.): cette problématique est donc comblée dans la plupart des cas par une redirection vers un formulaire d'authentification. Or avec un framework du type AngularJS, les pages sont générées dynamiquement sur le client à partir de templates nourries de données obtenues du serveur collectées grâce à du code client (Javasript).

D'un point de vue technique cette approche a évidemment énormément de sens, car:





  • Toutes les ressources clientes sont donc "cachables": templates, Javascript

  • A l'heure du Cloud (un buzzword, un), le coût d'hébergement est proportionnel à la sollicitation, il est tout naturel de transférer sur la machine du client (gratuite) toute charge éligilble

  • L'état du client est géré... sur le client (un truc de malade), permettant de réduire la pression sur la mémoire de la partie serveur (au point précédent on a économisé de la CPU, là on s'occupe de la RAM!). Sans compter que si la partie serveur ne maintient plus d'état, n'importe quelle instance pourra servir n'importe quelle requête... Nous parlons donc de conception Stateless (buzzword #2), permettant de déployer les noeuds en ayant aucun autre souci que la répartition de charge (pas de synchronisation entre les noeuds), m'amenant tout doucement vers le 3ème buzzword, la scalabilité horizontale.



En réfléchissant, si la traditionnelle redirection vers le formulaire de login ne semble pas être une option adaptée, que reste-t-il? Facile: le protocole HTTP prévoit au moins deux codes de retour liés aux problèmes de contrôles d'accès: 401, authentification requise, et 403, authentification refusée. Parfait, le premier m'explique que je dois présenter mes papiers et le second me signale que l'authentification fournie ne comprend pas l'accréditation nécessaire pour accéder à la ressource demandée.



Donc me voilà parti sur Google avec les mots clefs "Angular authentication 401" et je tombe sur un article fort intéressant:
http://www.espeo.pl/2012/02/26/authentication-in-angularjs-application . La démonstration est complète: toutes les requêtes tenues en échec suite à une erreur 401 sont bufferisées, en attente de la connexion de l'utilisateur et retentées le cas échéant. Et si on veut quelque chose de plus basique? Un peu plus newbie? Genre une redirection vers un formulaire de login à la première erreur 401 et une redirection a la racine suite à authentification? C'est sûrement pas aussi complet, mais l'aspect naïf permet de monter tranquillement en compétence sur AngularJS.



Avant toute chose, il nous faut un backend, mon choix c'est porté sur Play 2.1-RC1 car:



  • Il est stateless par essence

  • C'est hype, j'en avais envie et c'est moi le chef du blog (#noTroll)

Point de départ standard avec une application Play vide, ensuite on récupère une partie de l'authentification du sample
zentask en modifiant quelques aspects:



  • Tout d'abord, en cas de nécessité d'authentification, pas de redirection => 401:


  private def onUnauthorized(request: RequestHeader) = Results.Redirect(routes.Application.login)
devient:


  private def onUnauthorized(request: RequestHeader) = Results.Unauthorized





  • La démonstration nécessite une ressource protégée:




  def protectedResource = IsAuthenticated{
    username => _ =>
    Ok(Json.obj("test"->"1234"))
  }






  • Comme on veut faire une application cliente qui se connecte à un backend, pas d'utilisation des templates Play et modification de la route pour que la racine pointe sur une ressource statique:

GET     /                           controllers.Assets.at(path="/public", file="index.html")



Le code est disponible
ici.



Vérifications:





  • Pas autorisé:




$ curl http://localhost:9000/protectedResource -v
* About to connect() to localhost port 9000 (#0)
*   Trying ::1...
* connected
* Connected to localhost (::1) port 9000 (#0)
> GET /protectedResource HTTP/1.1
> User-Agent: curl/7.24.0 (x86_64-apple-darwin12.0) libcurl/7.24.0 OpenSSL/0.9.8r zlib/1.2.5
> Host: localhost:9000
> Accept: */*
>
< HTTP/1.1 401 Unauthorized
< Content-Length: 0
<
* Connection #0 to host localhost left intact
* Closing connection #0


L'application réfute bien l'accès avec un code 401.





  • Authentification:




$ curl http://localhost:9000/login -d 'mail=tony@stark.com&password=ironman' -v
* About to connect() to localhost port 9000 (#0)
*   Trying ::1...
* connected
* Connected to localhost (::1) port 9000 (#0)
> POST /login HTTP/1.1
> User-Agent: curl/7.24.0 (x86_64-apple-darwin12.0) libcurl/7.24.0 OpenSSL/0.9.8r zlib/1.2.5
> Host: localhost:9000
> Accept: */*
> Content-Length: 36
> Content-Type: application/x-www-form-urlencoded
>
* upload completely sent off: 36 out of 36 bytes
< HTTP/1.1 200 OK
< Set-Cookie: PLAY_SESSION=1701c43b7f845bdde0e38c0f43705d54b6815977-mail%3Atony%40stark.com; Path=/; HTTPOnly
< Content-Length: 0
<
* Connection #0 to host localhost left intact
* Closing connection #0

Code 200, un cookie signé de session en retour… tout va bien, donc on retente l'accès à la ressource protégée en présentant le sésame:




$ curl http://localhost:9000/protectedResource -b "PLAY_SESSION=1701c43b7f845bdde0e38c0f43705d54b6815977-mail%3Atony%40stark.com" -v
* About to connect() to localhost port 9000 (#0)
*   Trying ::1...
* connected
* Connected to localhost (::1) port 9000 (#0)
> GET /protectedResource HTTP/1.1
> User-Agent: curl/7.24.0 (x86_64-apple-darwin12.0) libcurl/7.24.0 OpenSSL/0.9.8r zlib/1.2.5
> Host: localhost:9000
> Accept: */*
> Cookie: PLAY_SESSION=1701c43b7f845bdde0e38c0f43705d54b6815977-mail%3Atony%40stark.com
>
< HTTP/1.1 200 OK
< Content-Type: application/json; charset=utf-8
< Content-Length: 15
<
* Connection #0 to host localhost left intact
{"test":"1234"}* Closing connection #0


On obtient donnée attendue, authentification réussie!



Maintenant je m'attaque au client:




<html ng-app="angularAuth" authenticator>
<head>
  <title>Angular authent</title>
    <script type="text/javascript" src="/public/javascripts/angular/angular.min.js"></script>
    <script type="text/javascript" src="/public/javascripts/angular/angular-resource.min.js"></script>
    <script type="text/javascript" src="/public/javascripts/app.js"></script>
    <script type="text/javascript" src="/public/javascripts/controllers.js"></script>
</head>
<body>
<h1>Simple authentication example</h1>
<div ng-view></div>
</body>
</html>


Mon module AngularJS est sommaire:




angular.module("angularAuth",['authServiceProvider']).
    config(['$routeProvider',function($routeProvider){
        $routeProvider.
            when("/", {templateUrl:"public/partials/protectedContent.html", controller:ProtectedCtrl}).
            when("/login",{templateUrl:"public/partials/login.html", controller:LoginCtrl}).
            otherwise({redirectTo:"/"})
    }]).
    directive('authenticator',function($location){
        return function(scope, elem, attrs){
            scope.$on('event:auth-loginRequired',function(){
                $location.path("/login")
            })
        }
    })  ;



En gros, quand "/" est demandé, affichage du template tirant la ressource protégée et lorsque "/login" est demandé, affichage du formulaire de login et redirection dans tous les autres cas.

Ensuite un listener va être positionné afin de réagir à l'évènement de demande d'authentification. L'utilisation de la directive permet de le placer au niveau du module, soit une fois pour toutes. Ne pas oublier de mentionner la directive dans le fichier html (moi, j'ai cherché un moment au début :-D).



Le concept présenté par Witold Szczerba est de tirer parti de la possibilité de poser des intercepteurs sur le service $http, c'est ce que je vais faire mais de façon plus naïve dans le module "authServiceProvider" injecté dans "angularAuth":




angular.module('authServiceProvider', []).
config(['$httpProvider', function($httpProvider) {

$httpProvider.responseInterceptors.push(function($q,$rootScope,$log){
function success(response) {
// $log.info(response)
return response
}

function error(response) {
if (response.status === 401) {
$log.error("401!!!!")
$rootScope.$broadcast('event:auth-loginRequired')
}
return $q.reject(response)
}

return function(promise) {
return promise.then(success, error)
}

})

}])



La fonction: si une requête retourne une erreur 401, alors l'évènement de demande d'authentification est propagé et stimule ainsi la directive et le formulaire de login apparaît. Je vous épargne un couplet sur l'API Promise d'AngularJS et cous encourage à aller consulter le Reference Guide.



Les contrôleurs du
formulaire d'authentification, de récupération de la ressource protégée et les
templates associés sont spartiates et consultables sur GitHub.



En introduction, j'ai présenté les arguments liés à la rationalisation de l'infrastructure d'hébergement, en revanche à l'heure actuelle il est clair que la charge de développement est supérieure comparée à une application dans laquelle les vues sont générées par le serveur: on y déclare simplement l'emplacement du formulaire de login.



La démo est testable sur
Heroku.












samedi 27 octobre 2012

Taille de session HTTP







Jusqu'ici pour déterminer la taille d'une session HTTP, j'utilisais Memory Analyzer Tool:



  • Acquisition de heap dump

  • Ouverture de la vue histogramme

  • Recherche de l'implémentation de la session: pour Tomcat c'est org.apache.catalina.session.StandardSession.

  • Clic droit sur la classe : List Objects / with outgoing references:






Et là j'obtiens une vue contenant toutes les sessions et surtout le graal la valeur Retained Heap:








Alors, ça permet d'obtenir l'information mais il faut reconnaître que MAT est assez lent pour parser les dumps, y compris sur mon Core i7 4 coeurs, 8Go, SSD, 48 soupapes, double vanos et pot Polini custom... Donc quand on veut connaître l'impact d'un clic sur la session, c'est pas terrible en terme d'efficacité.


J'ai trouvé un peu plus rapide avec VisualVM:




  • Génération du dump:







  • Ouverture de la console OQL et exécution de la requête 'select rsizeof(s) from org.apache.catalina.session.StandardSession s', rsizeof étant la fonction permettant de calculer la retained size :






On peut utiliser aussi
MessAdmin pour obtenir l'information directement, mais là j'avais pas envie de faire l'installation.







vendredi 10 août 2012

Java et VMWare




Les best practices pour virtualiser des applications java dans VMWare: 
http://kb.vmware.com/selfservice/microsites/search.do?cmd=displayKC&externalId=1008480


vendredi 16 mars 2012

JPA, Hibernate et requête constructeur




La spec JPA prévoit la possibilité de construire des POJO (non entité) à l'intérieur de requêtes JPQL (4.8.2 de la JSR317), ce qui s'avérer pratique pour requêter et construire des DTO en une étape. Je ne m'en prive donc pas :




TypedQuery<hoteldto> query = 
em.createQuery("select new org.blep.poc.hcq.HotelDto(h) from Hotel h",
HotelDto.class);




Et phénomène surprenant, en parcourant la liste de résultats, je m'aperçois que les entités identifiées dans une première requête et sont remontées unitairement :




Hibernate: /* select  new org.blep.poc.hcq.HotelDto(h) from Hotel h */
   select hotel0_.id as col_0_0_ from Hotel hotel0_
Hibernate: /* load org.blep.poc.hcq.Hotel */
  select hotel0_.id as id0_0_, hotel0_.address as address0_0_, hotel0_.city as city0_0_, hotel0_.name as name0_0_, hotel0_.price as price0_0_, hotel0_.state as state0_0_ from Hotel hotel0_ where hotel0_.id=?




Je me retrouve donc avec (nombre d'entités selectionnées + 1) requêtes soumises à la BDD (une requête de surface - shallow query- et les requêtes de chargement), ce qui peut vite devenir un frein pour les performances.



La doc Hibernate est plus que succincte sur le sujet des "constructor expressions", en gros c'est supporté mais il n'y a pas plus d'info qui explique ce comportement. Si la doc n'aide pas, il reste les retours d'expériences des autres utilisateurs dans les forums ou les blogs et en dernier lieu les bugs...  En fouinant un peu je trouve 
https://hibernate.onjira.com/browse/HHH-544 qui décrit parfaitement mon problème et Sir GK Himself répond en 2005:



 To be clear, all "select new" queries are considered "shallow" by the parser.
La date, statut de la JIRA et la réponse ne permettent pas d'envisager la négociation sur le sujet ni aucun tuning d'ailleurs pour influer sur le comportement! Il ne reste qu'une seule autre voie, la recherche des solutions de contournement...



La première tentée est l'utilisation de l'API Criteria, je ne suis pas fan mais si ça marche, comme le code reste inscrit dans JPA, pourquoi pas:






CriteriaBuilder criteriaBuilder = em.getCriteriaBuilder();
CriteriaQuery<HotelDto> cq = criteriaBuilder.createQuery(HotelDto.class);
Root<Hotel> from = cq.from(Hotel.class);
cq.select(criteriaBuilder.construct(HotelDto.class, from));
TypedQuery<HotelDto> query = em.createQuery(cq);






Rien que de le coder j'ai mal... mais en plus la sortie SQL montre que le Criteria est interprété pour générer du JPQL (tiens d'ailleurs ça me donne une raison de plus de ne pas aimer cette API!):






Hibernate: /* select new org.blep.poc.hcq.HotelDto(generatedAlias0) from Hotel as generatedAlias0 */ select hotel0_.id as col_0_0_ from Hotel hotel0_


Donc aucun intérêt puisque le comportement n'évolue pas.






La mort dans l'âme je me résouds à aller au delà de JPA et à m'adresser directement à Hibernate et il existe effectivement un moyen de combler le besoin mais pas dans le cadre d'un constructeur:






Query query = em.createQuery("select h as hotel from Hotel h");
org.hibernate.Query unwrapped = query.unwrap(org.hibernate.Query.class);
unwrapped.setResultTransformer(Transformers.aliasToBean(HotelDto.class));







Je sens d'ici les picotement dans les yeux de mon lecteur (un, c'est toute mon ambition en terme d'auditoire pour le moment!), toutefois les données sont obtenues en un seul SQL. Au rayon des contraintes, le DTO n'est pas peuplé par le constructeur mais par mutateur, ce qui implique qu'il doit y avoir un constructeur sans argument et que le mutateur est lié à l'alias dans le JPQL (ie le DTO doit présenter la méthode setHotel(Hotel). Le fait que le DTO puisse être construit sans argument ouvre la possibilité d'avoir un objet inconsistant, ce n'est pas souhaitable mais tellement fréquent... 






Possibilité suivante, utiliser un implémentation propre de Collection<HotelDto> encapsulant une Collection<Hotel> comme déléguée (code disponible sur
https://github.com/bleporini/JPA-Hibernate-shallow-constructor-query/blob/master/src/main/java/org/blep/poc/hcq/HotelDtoCollection.java). Du coup le code devient:





HotelDtoCollection hotels = new HotelDtoCollection(em.createQuery("select h from Hotel h", Hotel.class).getResultList());


C'est correct, ne nécessite pas de révéler une implémentation sous-jacente mais clairement ça fait pas mal de code boiler plate... Ca serait pas mal de trouver quelquechose de plus élégant.



Donc voici la voie basée sur Google Guava:





List<Hotel> resultList = em.createQuery("select h from Hotel h", Hotel.class).getResultList();
List<HotelDto> dtos = Lists.transform(resultList, new Function<Hotel, HotelDto>() {
    public HotelDto apply(@Nullable Hotel input) {
        return new HotelDto(input);
    }
});








C'est une possibilité aussi pertinente que celle de la collection déléguée mais nettement plus concise, la doc stipule que la liste résultante est peuplée tardivement, c'est parfait!



Côté performances, pour une requête ramenant 100 entités d'une base H2 embarquée, la solution du transformateur Hibernate prend 23ms, la collection déléguée 19ms et Guava 19ms sur mon poste.




















jeudi 19 mai 2011

Création d'une mini PKI




Au moment d'ajouter la couche SSL à une application web, vient toujours la même questions "Comment ça marche déjà? Je l'ai déjà fait il y a plusieurs années, je me suis pris la tête mais je ne me rappelle plus comment!"... et je recommence à zéro! Alors voici un lien pour créer sa propre AC.



A des fins de tests on créé souvent des certificats autosignés (self-signed) car c'est facile. Toutefois pour rendre le test plus proche de la situation réelle, il peu être interressant de travailler avec des certificats qui sont signés par une autorité de certification, mais comment créer une autorité de certification??? Réponse sur 
http://artisan.karma-lab.net/node/1153 !


samedi 26 février 2011

MVP et GWT




J'ai trouvé deux articles intéressants sur le pattern MVP sur ce blog:




http://www.mikaelkrok.net/le-design-pattern-mvp-et-gwt-1-introduction



et




http://www.mikaelkrok.net/le-design-pattern-mvp-et-gwt-2-mvp-en-detail





Et voilà comment on fait un billet marque page!


vendredi 18 février 2011

MDB: utiliser des EJB sécurisés







Allez encore un petit billet sur la sécurité et les MDB.

Lorsqu'un MDB reçoit un message, il peut arriver (et même dans la plupart des cas!) qu'il doive consommer des services qui sont exposés par d'autres EJB. Supposons que l'EJB en question comprenne un contrôle d'accès sur ses méthodes:





@Stateless
@Local(LocalMyService.class)
@DeclareRoles("authenticated")
@RolesAllowed("authenticated")
public class MyServiceImpl implements LocalMyService {




... et que ce service est injecté dans un MDB:





@MessageDriven(activationConfig = {
@ActivationConfigProperty(propertyName = "user",
propertyValue = "mdbuser"),
@ActivationConfigProperty(propertyName = "password",
propertyValue = "mdbpassword"),
@ActivationConfigProperty(propertyName = "acknowledgeMode",
propertyValue = "Auto-acknowledge"),
@ActivationConfigProperty(propertyName = "destinationType",
propertyValue = "javax.jms.Queue"),
@ActivationConfigProperty(propertyName = "destination",
propertyValue = "/queue/myQueue")
})
public class MyMDB implements MessageListener {
@EJB
private LocalMyService service;

@Override
public void onMessage(Message message) {
[...]
service.myMethod();










Là l'affaire se corse car les messages JMS n'embarquent pas de contexte de sécurité (après recherche et sauf erreur de ma part), et lors de la réception d'un message le MDB se verra l'accès refusé au service. Et bien c'est
presque pas grave, car la spec EJB couvre ce problème et permet la substitution de rôle avec l'annotation javax.annotation.security.@RunAs qui prend en paramètre le nom du rôle à donner au bean (ici le MDB):








@MessageDriven(activationConfig = {
@ActivationConfigProperty(propertyName = "user",
propertyValue = "mdbuser"),
@ActivationConfigProperty(propertyName = "password",
propertyValue = "mdbpassword"),
@ActivationConfigProperty(propertyName = "acknowledgeMode",
propertyValue = "Auto-acknowledge"),
@ActivationConfigProperty(propertyName = "destinationType",
propertyValue = "javax.jms.Queue"),
@ActivationConfigProperty(propertyName = "destination",
propertyValue = "/queue/myQueue")
})
@RunAs("authenticated")
public class MyMDB implements MessageListener {
@EJB
private LocalMyService service;

@Override
public void onMessage(Message message) {
[...]
service.myMethod();













Il est intéressant de noter que j'ai écrit "
presque pas grave"! Presque, car le conteneur JEE sélectionné pour le projet en question était JBoss 6.0.0.Final qui est directement impacté par le bug 
EJBTHREE-1945. Le sujet de bug est que le rôle est correctement substitué dans le premier appel de méthode sécurisé, mais si cette méthode fait elle-même appel à une autre méthode sécurisée , le rôle est perdu... autant dire que tout l'intérêt de RunAs est caduque! 






J'ai donc demandé s'il existait un moyen de contournement et je me suis inspiré de la réponse pour mon besoin en créant un intercepteur assigné au MDB qui authentifie le MDB avant l'invocation de la méthode onMessage:









public class RunAsInterceptor {
@AroundInvoke
public Object intercept(InvocationContext ctx) throws Throwable {
SecurityClient securityClient = null;

try {
securityClient = SecurityClientFactory.getSecurityClient();
securityClient.setSimple(userName, password);
securityClient.login();

return ctx.proceed();

} finally {
if (securityClient != null) {
securityClient.logout();
}
}
}
}







L'ombre au tableau est qu'une classe hors spec JEE issue de l'API propre à JBoss est impliquée (SecurityClient), je n'ai pas poussé pour trouver un moyen d'authentification avec les outils JEE stricts, si quelqu'un a ça en stock, je suis preneur! Je vous laisse le soin de définir comment paramétrer userName et password, moi je l'ai fait avec le chargement de propriétés dans le constructeur sans arguments. 






L'utilisation dans le MDB:






@MessageDriven(activationConfig = {
[...]
})
@Interceptors({RunAsInterceptor.class})
public class MyMDB implements MessageListener {



L'avantage de cette conception est que lorsque les gens de JBoss se décideront à résoudre ce problème qui date de 2009, il n'y aura qu'à dégager l'intercepteur et le remplacer par @Runas!



Mais bon, c'est la tour de Babel!