mercredi 17 février 2010

Wicket et SSO

Dans un billet précedent, nous avons vu comment utiliser wicket auth-roles pour gérer l'authentification dans une application Wicket. Cette solution a l'avantage d'être très simple à mettre en oeuvre, mais a l'inconvénient de ne pas être très flexible : en effet, elle nous oblige à utiliser un système d'authentification classique à l'aide d'un formulaire de login - mot de passe.
Nous irons plus loin aujourd'hui en définissant nous même les règles de sécurité dans l'application, en nous appuyant sur un système d'authentification externe, par exemple un SSO.
Pour rappel un SSO est un système d'authentification unique dans un système d'information. Par exemple sur l'intranet de votre enterprise, imaginez que vous puissiez vous logger une fois au portail global, vous serez ensuite authentifié pour toutes les autres applications du SI. Un peu comme quand vous vous connectez à Gmail et que vous pouvez ensuite accéder à Picasa ou Google docs avec votre compte google sans retaper votre mot de passe.

Redéfinition de la session Wicket
Nous allons créer notre propose classe de session pour pouvoir stocker l'utilisateur du SSO durant toute la durée de la connection d'un utilisateur :

public class MySession extends WebSession {
   
private SsoUser ssoUser;
public boolean isAdmin(){
 return ssoUser.hasRole("admin");
}

public isAuthenticated(){
return ssoUser != null;
}
   //getters and setters
   ...
}


Les méthodes isAdmin() et isAuthenticated() nous seront utiles par la suite.


Modification de la classe WicketApplication
Dans notre exemple, nous imaginons que le SSO nous envoie dans une entête HTTP l'utilisateur connecté.
On suppose aussi que le SSO propose un bibliothèque Java pour récupérer les informations depuis la requête HTTP.
On récupère donc le login dans la requête HTTP et on le stocke dans la session wicket en overridant la méthode newSession de la classe principale de notre application :

public Session newSession(Request request, Response response) {
  SsoUser ssoUser = null;
  HttpServletRequest httpServletRequest = null;
  //Récupération des informations de login dans la requete http
  MySession session = new MySession(request);
  httpServletRequest = ((WebRequest) request).getHttpServletRequest();
  if (!DEVELOPMENT.equalsIgnoreCase(getConfigurationType())) {   
   ssoUser = ssoUtil.getUser(httpServletRequest);   
  }
  else{
   //DEVELOPMENT MODE   
   //Ici on peut mettre ce que l'on veut pour s'authentifier avec un compte par défaut. Ex :
   ssoUser = new SsoUser(Sso.ADMIN);
  }
  
  session.setUser(ssoUser);
  return session;
}
Comme vous pouvez le voir, on a désactivé le SSO en mode développement pour pouvoir être connectés directement en admin. Au besoin, on peut toujours forcer un utilisateur différent en passant un nom d'utilisateur dans la requête HTTP. Pour cela, on pourrait vérifier la présence d'un paramètre de requête dans l'URL puis enregister un utilisateur différent en session si ce paramètre est présent. La présence d'un paramètre peut se tester comme ceci :
String user = httpServletRequest.getParameter("user");
Pour voir comment configurer votre application en mode développement, c'est ici.


Redéfinition de la stratégie de sécurité de l'application
Pour terminer, nous allons redéfinir la stratégie de sécurité de l'application en implémentant les interfaces IAuthorizationStrategy et IUnauthorizedComponentInstantiationListener. Nous allons dire à notre classe de n'autoriser l'instantiation des pages que lorsque l'utilisateur SSO est bien identifié et stocké en session. 
De plus, nous allons créer une annotation à placer sur les pages réservées à l'administrateur (merci au livre Wicket in Action pour cette astuce!). Lorsque cette annotation sera présente, le rôle de l'utilisateur sera vérifié avant l'affichage.
public final class MyAuthorizationStrategy implements
    IAuthorizationStrategy,
    IUnauthorizedComponentInstantiationListener {

  public boolean isActionAuthorized(Component component, Action action) {

    if (action.equals(Component.RENDER)) {
      Class c = component.getClass();
      AdminOnly adminOnly = c.getAnnotation(AdminOnly.class);
      //Si l'annotation est présente on vérifie que l'utilisateur est admin
      if (adminOnly != null) {
        SsoUser ssoUser = MySession.get().getSsoUser();
        return (ssoUser != null && user.isAdmin());
      }
    }
    return true;
  }

public boolean isInstantiationAuthorized(Class componentClass) {
return MySession.get().isAuthenticated();
}

  public void onUnauthorizedInstantiation(Component component) {
    throw new RestartResponseAtInterceptPageException(HomePage.class);
  }
}

NB: ici on bloque tout si l'utilisateur n'est pas enregistré mais on pourrait bloquer seulement certaines pages (celles qui héritent de PrivatePage par exemple) comme ceci :

public boolean isInstantiationAuthorized(Class componentClass) {

    if (PrivatePage.class.isAssignableFrom(componentClass)) {
       //vérification dans la session
       return MySession.get().isAuthenticated();
    }

    return true;
  }
Et pour finir, le code de l'annotation à placer sur les pages d'admin :

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Inherited
public @interface AdminOnly {
  
}

@AdminOnly
public class AdminPage extends WebPage(){

...
}
Ca y'est, vous êtes prêt à intégrer votre application Wicket avec le SSO de votre entreprise!
Si vous avez déjà rencontré cette problématique et que vous avez procédé autrement, n'hésitez pas à me donner vos astuces!