Java:JAAS

De WIKI.minetti.org
Révision de 7 avril 2016 à 17:33 par Jp (discussion | contributions) (Page créée avec « == But == JAAS (Java Authentication and Authorization Service) est un framework intégré au JRE permettant l'authentification et l'autorisation des utilisateurs. == Les... »)

(diff) ← Version précédente | Voir la version courante (diff) | Version suivante → (diff)
Aller à : navigation, rechercher

But

JAAS (Java Authentication and Authorization Service) est un framework intégré au JRE permettant l'authentification et l'autorisation des utilisateurs.

Les interfaces et classes JAAS

Interfaces et classes Explications
javax.security.auth.Subject Classe sujet représentant un groupement d'informations relatives à une seule entité, comme une personne. Il peut potentiellement avoir de multiples identités.
java.security.Principal Interface représentant une identité d'un sujet. Les classes implémentant l'interface Principal doivent aussi implémenter l'interface java.io.Serializable.
javax.security.auth.Refreshable

javax.security.auth.Destroyable

Interfaces représentant des pouvoirs pour fournir à une classe la capacité de se rafraîchir ou de détruire son contenu.

Authentification

Interfaces et classes Explications
javax.security.auth.login.LoginContext La classe LoginContext décrit les méthodes de base utilisées pour authentifier les sujets et fournit un moyen de développer une application indépendante de la technologie d'authentification sous-jacent. Le LoginContext d'une application peut charger plusieurs LoginModules (authentification empilés). Par exemple, on pourrait avoir un LoginModule LDAP et un LoginModule carte à puce.
javax.security.auth.spi.LoginModule L'interface LoginModule permet de définir un service d'authentification (LDAP, Kerberos, carte à puce, etc...).
javax.security.auth.callback.CallbackHandler L'interface CallbackHandler permet l'implémentation d'une classe chargée de transmettre des informations à l'application par les LoginModule. Elle permet par exemple la saisie du login et du mot de passe.
javax.security.auth.callback.Callback L'interface Callback permet l'implémentation d'une classe contenant une information à transmettre à l'application via le CallbackHandler. En principe, elle contient le message d'invite de saisie (prompt) et la donnée saisie.
javax.security.auth.login.Configuration La classe abstraite Configuration est utilisé par le LoginContext pour charger sa configuration.

Autorisation

Interfaces et classes Explications
java.security.PrivilegedAction L'interface PrivilegedAction permet l'implémentation d'une classe chargée d'accéder à des ressources protégées sous les identités d'un sujet (Subject). Elle est utilisée par les méthodes statiques doAs et doAsPrivileged de la classe Subject.
java.security.PrivilegedExceptionAction L'interface PrivilegedExceptionAction permet l'implémentation d'une classe chargée d'accéder à des ressources protégées sous les identités d'un sujet (Subject) et dont le code est succeptible de déclencher des exceptions. Elle est utilisée par les méthodes statiques doAs et doAsPrivileged de la classe Subject.
java.security.AllPermission La classe AllPermission permet de donner des droits sur l'ensemble des ressources.
java.io.FilePermission La classe FilePermission permet de donner des droits de lecture (read), d'écriture (write), d'exécution (execute) et de suppression (delete) sur des fichiers.
java.security.Permission Classe abstraite utilisé pour implémenter de nouvelles classe permission.

Configuration du LoginContext

Par défaut, le LoginContext recherche sa configuration dans un fichier (chargé par la classe com.sun.security.auth.login.ConfigFile) qui est sous la forme:

<Name> {
        <LoginModules>  <Flag>    <ModuleOptions>;
        <LoginModules>  <Flag>    <ModuleOptions>;
        <LoginModules>  <Flag>    <ModuleOptions>;
};
<Name> {
        <LoginModules>  <Flag>    <ModuleOptions>;
        <LoginModules>  <Flag>    <ModuleOptions>;
};
other {
        <LoginModules>  <Flag>    <ModuleOptions>;
        <LoginModules>  <Flag>    <ModuleOptions>;
};
Flag Explications
Required Le LoginModule est nécessaire pour réussir. Dans tous les cas, l'authentification continuera avec les LoginModules suivants de la liste.
Requisite Le LoginModule est nécessaire pour réussir.
  • S'il réussi, l'authentification continuera avec les LoginModules suivants de la liste.
  • S'il échoue, l'authentification est interrompu (bloquant sur échec).
Sufficient Le LoginModule n'est pas tenu de réussir.
  • S'il réussi, l'authentification est interrompu (bloquant sur succès).
  • S'il échoue, l'authentification continuera avec les LoginModules suivants de la liste.
Optional Le LoginModule n'est pas tenu de réussir. Dans tous les cas, l'authentification continuera avec les LoginModules suivants de la liste.


Exemple d'application mettant en œuvre JAAS

Authentification

// Création du LoginContext
LoginContext loginContext = null;
try {
   final URI uri = getClass ().getResource ("/jaas.config").toURI ();
   loginContext = new LoginContext ("Test", null, new TestCallbackHandler (), new ConfigFile (uri));
}
catch (LoginException e) {
   System.err.println ("Cannot create LoginContext: " + e.getMessage ());
   System.exit (-1);
}
catch (SecurityException e) {
   System.err.println ("Cannot create LoginContext: " + e.getMessage ());
   System.exit (-1);
}

// Authentification
try {
   loginContext.login ();
}
catch (LoginException e) {
   System.err.println ("Authentication failed:" + e.getMessage ());
   System.exit (-1);
}
System.out.println ("Authentication succeeded!");

Classe TestCallbackHandler

/**
 * Test callback handler.
 * @author JPM
 */
public final class TestCallbackHandler
         implements CallbackHandler
{

   /**
    * Constructor.
    */
   public TestCallbackHandler ()
   {
      super ();
   }

   /**
    * {@inheritDoc}
    * @see javax.security.auth.callback.CallbackHandler#handle(javax.security.auth.callback.Callback[])
    */
   public void handle (final Callback[] callbacks) throws IOException, UnsupportedCallbackException
   {
      for (int i = 0; i < callbacks.length; i++) {
         if (callbacks [i] instanceof TextOutputCallback) {
            print ((TextOutputCallback) callbacks [i]);
         }
         else if (callbacks [i] instanceof NameCallback) {
            prompt ((NameCallback) callbacks [i]);
         }
         else if (callbacks [i] instanceof PasswordCallback) {
            prompt ((PasswordCallback) callbacks [i]);
         }
         else {
            throw new UnsupportedCallbackException (callbacks [i], "Unrecognized Callback");
         }
      }
   }

   /**
    * Displays a message according to a specified type.
    * @param callback Text output callback.
    * @throws IOException Exception when the message type is unrecognized.
    */
   private void print (final TextOutputCallback callback) throws IOException
   {
      switch (callback.getMessageType ()) {
         case TextOutputCallback.INFORMATION:
            System.out.println (callback.getMessage ());
            break;
         case TextOutputCallback.ERROR:
            System.out.println ("ERROR: " + callback.getMessage ());
            break;
         case TextOutputCallback.WARNING:
            System.out.println ("WARNING: " + callback.getMessage ());
            break;
         default:
            throw new IOException ("Unsupported message type: " + callback.getMessageType ());
      }
   }

   /**
    * Prompts the user for a username.
    * @param callback Name callback.
    * @throws IOException Exception when an error occurred.
    */
   private void prompt (final NameCallback callback) throws IOException
   {
      System.err.print (callback.getPrompt ());
      System.err.flush ();
      callback.setName ((new BufferedReader (new InputStreamReader (System.in))).readLine ());
   }

   /**
    * Prompts the user for a password.
    * @param callback Password callback.
    * @throws IOException Exception when an error occurred.
    */
   private void prompt (final PasswordCallback callback) throws IOException
   {
      System.err.print (callback.getPrompt ());
      System.err.flush ();
      callback.setPassword (readPassword (System.in));
   }

   /**
    * Reads an user password.
    * @param in Input stream.
    * @return User password.
    * @throws IOException Exception when an error occurred.
    */
   private char[] readPassword (InputStream in) throws IOException
   {
      char[] lineBuffer = new char [128];
      char[] buf = new char [128];
      int room = buf.length;
      int offset = 0;
      int c;
      boolean edition = true;
      while (edition) {
         switch (c = in.read ()) {
            case -1:
            case '\n':
               edition = false;
               break;
            case '\r':
               int c2 = in.read ();
               if ((c2 != '\n') && (c2 != -1)) {
                  if (!(in instanceof PushbackInputStream)) {
                     in = new PushbackInputStream (in);
                  }
                  ((PushbackInputStream) in).unread (c2);
               }
               else {
                  edition = false;
                  break;
               }
            default:
               if (--room < 0) {
                  buf = new char [offset + 128];
                  room = buf.length - offset - 1;
                  System.arraycopy (lineBuffer, 0, buf, 0, offset);
                  Arrays.fill (lineBuffer, ' ');
                  lineBuffer = buf;
               }
               buf [offset++] = (char) c;
               break;
         }
      }
      char[] ret = null;
      if (offset != 0) {
         ret = new char [offset];
         System.arraycopy (buf, 0, ret, 0, offset);
         Arrays.fill (buf, ' ');
      }
      return ret;
   }

}

Fichier jaas.config

Pour que le LoginContext puisse trouver le fichier de configuration (jaas.config) à travers la classe ConfigFile, il faut renseigner la propriété système java.security.auth.login.config en indiquant le chemin et le nom du fichier. Une alternative consisterait de passer l'URI du fichier de configuration en paramètre du constructeur de la classe ConfigFile.

Test {

  com.sun.security.auth.module.LdapLoginModule REQUISITE
                        userProvider="ldap://ldap.srv.minetti.org/ou=people,ou=localnet,dc=minetti,dc=org"
                        userFilter="(&(uid={USERNAME})(objectClass=inetOrgPerson))"
                        useSSL=false
                        debug=true;

  com.sun.security.auth.module.UnixLoginModule REQUIRED
                        debug=true;

};

Autorisation

Tout d'abord, on procède à l'initialisation du fichier des autorisations:

final URL url = getClass ().getResource ("/jaas.policy");
Policy.setPolicy (new PolicyFile (url));

Ensuite, on exécute du code protégé par:

Subject.doAsPrivileged (loginContext.getSubject (), new TestPrivilegedAction (), null);

Classe TestPrivilegedAction

Dans notre exemple, le code protégé doit se trouver dans la classe TestPrivilegedAction. Deux cas de figures se présentent:

public final class TestPrivilegedAction
         implements PrivilegedExceptionAction<String>
{

   public String run () throws Exception
   {
      AccessController.checkPermission (new TestPermission ("user", "create"));

      ...

      return uid;
   }

}

Avant le traitement, on fera toujours un appel à la méthode checkPermission(...) de la classe java.security.AccessController afin de vérifier l'autorisation requise pour exécuter le traitement.

Classe TestPermission

public final class TestPermission
         extends Permission
{

   private static final long serialVersionUID = -5412174518393360973L;

   private final List<String> actionList;

   public TestPermission (final String name, final String actions)
   {
      this (name, new ArrayList<String> ());
      if ((actions != null) && (!"".equals (actions.trim ()))) {
         final String[] actionArray = actions.split (",");
         for (int i = 0; i < actionArray.length; i++) {
            this.actionList.add (actionArray [i].trim ().toLowerCase ());
         }
      }
   }

   public TestPermission (final String name, final List<String> actionList)
   {
      super (name.trim ());
      this.actionList = actionList;
   }

   @Override
   public boolean implies (final Permission permission)
   {
      boolean result = false;
      if (permission instanceof TestPermission) {
         if (getName ().equals (permission.getName ())) {
            final TestPermission otherPermission = (TestPermission) permission;
            result = this.actionList.containsAll (otherPermission.actionList);
         }
      }
      return result;
   }

   protected List<String> getActionList ()
   {
      return this.actionList;
   }

   @Override
   public String getActions ()
   {
      String actions = null;
      for (final String action : this.actionList) {
         if (actions == null) {
            actions = action;
         }
         else {
            actions += "," + action;
         }
      }
      return actions;
   }

   @Override
   public PermissionCollection newPermissionCollection ()
   {
      return new TestPermissionCollection ();
   }

   @Override
   public int hashCode ()
   {
      final int prime = 31;
      int result = 1;
      result = (prime * result) + ((this.actionList == null) ? 0 : this.actionList.hashCode ());
      result = (prime * result) + ((getName () == null) ? 0 : getName ().hashCode ());
      return result;
   }

   @Override
   public boolean equals (final Object obj)
   {
      boolean result = false;
      if (obj == this) {
         result = true;
      }
      else if ((getName () != null) && (this.actionList != null) && (obj instanceof TestPermission)) {
         final TestPermission permission = (TestPermission) obj;
         if (getName ().equals (permission.getName ())) {
            result = this.actionList.equals (permission.actionList);
         }
      }
      return result;
   }

}
public final class TestPermissionCollection
         extends PermissionCollection
{

   private static final long serialVersionUID = 3535723239185980476L;

   private transient Map<String, Permission> permissionMap = new HashMap<String, Permission> ();

   private static final ObjectStreamField[] serialPersistentFields = {new ObjectStreamField ("permissions", Hashtable.class)};

   public TestPermissionCollection ()
   {
      super ();
   }

   @Override
   public void add (final Permission permission)
   {
      if (!(permission instanceof TestPermission)) {
         throw new IllegalArgumentException ("Invalid permission: " + permission);
      }
      if (isReadOnly ()) {
         throw new SecurityException ("Attempt to add a Permission to a readonly PermissionCollection");
      }
      final TestPermission permissionToAdd = (TestPermission) permission;
      final String name = permissionToAdd.getName ();
      synchronized (this) {
         final TestPermission existing = (TestPermission) this.permissionMap.get (name);
         if (existing != null) {
            final List<String> actionList = existing.getActionList ();
            for (final String actionToAdd : permissionToAdd.getActionList ()) {
               if (!actionList.contains (actionToAdd)) {
                  actionList.add (actionToAdd);
               }
            }
            this.permissionMap.put (name, new TestPermission (name, actionList));
         }
         else {
            this.permissionMap.put (name, permissionToAdd);
         }
      }
   }

   @Override
   public boolean implies (final Permission permission)
   {
      boolean result = false;
      if (permission instanceof TestPermission) {
         synchronized (this) {
            final TestPermission perm = (TestPermission) this.permissionMap.get (permission.getName ());
            if (perm != null) {
               result = perm.implies (permission);
            }
         }
      }
      return result;
   }

   @Override
   public Enumeration<Permission> elements ()
   {
      Enumeration<Permission> result;
      synchronized (this) {
         result = Collections.enumeration (this.permissionMap.values ());
      }
      return result;
   }

   private void writeObject (final ObjectOutputStream out) throws IOException
   {
      final Hashtable<String, Permission> permissions = new Hashtable<String, Permission> (this.permissionMap.size () * 2);
      synchronized (this) {
         permissions.putAll (this.permissionMap);
      }
      final ObjectOutputStream.PutField pfields = out.putFields ();
      pfields.put ("permissions", permissions);
      out.writeFields ();
   }

   @SuppressWarnings("unchecked")
   private void readObject (final ObjectInputStream in) throws IOException, ClassNotFoundException
   {
      final ObjectInputStream.GetField gfields = in.readFields ();
      final Hashtable<String, Permission> permissions = (Hashtable<String, Permission>) gfields.get ("permissions", null);
      this.permissionMap = new HashMap<String, Permission> (permissions.size () * 2);
      this.permissionMap.putAll (permissions);
   }

}

Fichier jaas.policy

grant Principal com.sun.security.auth.LdapPrincipal "cn=users,ou=group,ou=test,dc=minetti,dc=org" {
  permission org.minetti.test.auth.security.TestPermission "userProfile", "modify";
};

grant Principal com.sun.security.auth.LdapPrincipal "cn=admins,ou=group,ou=test,dc=minetti,dc=org" {
  permission org.minetti.test.auth.security.TestPermission "user", "find,list,create,modify,delete,assign_role";
};

Dans notre exemple, seul les utilisateurs membres du groupe admins pourront exécuter notre traitement (TestPrivilegedAction).

Lancement de l'application

Pour lancer l'application, on tape la commande suivante sans oublier d'indiquer le chemin et le nom du fichier de configuration:

java -Djava.security.auth.login.config=jaas.config -jar test.jar

Choix de la Configuration

Classes Explications
com.sun.security.auth.login.ConfigFile Classe permettant de charger la configuration du LoginContext à partir d'un fichier. Le chemin et le nom de ce fichier doit être indiqué dans la propriété système java.security.auth.login.config si l'URI de ce fichier n'est pas passé en paramètre lors de l'appel au constructeur de la classe.

Choix des LoginModule

Classes Explications
com.sun.security.auth.module.LdapLoginModule Classe LoginModule permettant d'authentifier des utilisateurs auprès d'un serveur LDAP. Ce module demande le nom d'utilisateur (NameCallback) et son mot de passe (PasswordCallback). En cas de succès, un UserPrincipal et un LdapPrincipal sont ajoutés au Subject.
com.sun.security.auth.module.Krb5LoginModule Classe LoginModule utilisant le protocole Kerberos pour authentifier les utilisateurs. Ce module demande le nom d'utilisateur Kerberos (NameCallback) et son mot de passe (PasswordCallback). En cas de succès, un KerberosPrincipal est ajoutés au Subject, ainsi qu'un KerberosTicket dans les private credentials.
com.sun.security.auth.module.KeyStoreLoginModule
com.sun.security.auth.module.UnixLoginModule
com.sun.security.auth.module.SolarisLoginModule
com.sun.security.auth.module.NTLoginModule
com.sun.security.auth.module.JndiLoginModule

Exemple de LoginModule

Il est possible de définir ses propres LoginModule si ceux existant ne suffisent pas:

package org.minetti.security.auth.module;

import java.io.IOException;
import java.util.Map;
import javax.security.auth.Subject;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.NameCallback;
import javax.security.auth.callback.PasswordCallback;
import javax.security.auth.callback.UnsupportedCallbackException;
import javax.security.auth.login.FailedLoginException;
import javax.security.auth.login.LoginException;
import javax.security.auth.spi.LoginModule;
import org.minetti.security.auth.TestPrincipal;

/**
 * JAAS login module: Example.
 * @author JPM
 */
public final class TestLoginModule
         implements LoginModule
{

   /**
    * Subject to be authenticated.
    */
   private Subject subject = null;

   /**
    * CallbackHandler for communicating with the end user.
    */
   private CallbackHandler callbackHandler = null;

   /**
    * State shared with other configured LoginModules.
    */
   private Map<String, ?> sharedState = null;

   /**
    * TRUE if the module is in debug mode.
    */
   private boolean debug = false;

   /**
    * TRUE if the authentication is a success.
    */
   private boolean succeeded = false;

   /**
    * TRUE if the authentication is committed.
    */
   private boolean commitSucceeded = false;

   /**
    * Username.
    */
   private String username;

   /**
    * Password.
    */
   private char[] password;

   /**
    * Principal of user.
    */
   private TestPrincipal userPrincipal = null;

   /**
    * {@inheritDoc}
    * @see javax.security.auth.spi.LoginModule#initialize(javax.security.auth.Subject,
    *      javax.security.auth.callback.CallbackHandler, java.util.Map, java.util.Map)
    */
   public void initialize (final Subject subject, final CallbackHandler callbackHandler, final Map<String, ?> sharedState,
            final Map<String, ?> options)
   {

      this.subject = subject;
      this.callbackHandler = callbackHandler;
      this.sharedState = sharedState;

      // Debug option
      this.debug = "true".equalsIgnoreCase ((String) options.get ("debug"));

   }

   /**
    * {@inheritDoc}
    * @see javax.security.auth.spi.LoginModule#login()
    */
   public boolean login () throws LoginException
   {
       if (this.callbackHandler == null) {
         throw new LoginException ("Error: no CallbackHandler available to garner authentication information from the user");
      }

      // Saisie utilisateur
      Callback[] callbacks = new Callback [2];
      callbacks [0] = new NameCallback ("Utilisateur: ");
      callbacks [1] = new PasswordCallback ("Mot de passe: ");
      try {
         this.callbackHandler.handle (callbacks);
         this.username = ((NameCallback) callbacks [0]).getName ();
         char[] tmpPassword = ((PasswordCallback)callbacks[1]).getPassword();
         if (tmpPassword == null) {
            tmpPassword = new char[0];
         }
         this.password = new char[tmpPassword.length];
         System.arraycopy(tmpPassword, 0, this.password, 0, tmpPassword.length);
         ((PasswordCallback)callbacks[1]).clearPassword();
      }
      catch (IOException e) {
         throw new LoginException (e.toString ());
      }
      catch (UnsupportedCallbackException e) {
         throw new LoginException ("Error: " + e.getCallback ().toString () + " not available to garner authentication information from the user");
      }
      if (this.debug) {
         System.out.println ("\t\t[TestLoginModule] username: " + this.username);
      }

      // Account reading
      ...

      // Verification
      this.succeeded = ...
      if (this.succeeded) {
         if (this.debug) {
            System.out.println ("\t\t[TestLoginModule] authentication succeeded");
         }
      }
      else {
         if (debug) {
            System.out.println ("\t\t[TestLoginModule] authentication failed");
         }
         cleanState ();
         throw new FailedLoginException ("Password incorrect");
      }
      return this.succeeded;
   }

   /**
    * {@inheritDoc}
    * @see javax.security.auth.spi.LoginModule#commit()
    */
   public boolean commit () throws LoginException
   {
      boolean result = false;
      if (this.succeeded) {
         if (this.subject.isReadOnly ()) {
            cleanState ();
            throw new LoginException ("Subject is read-only");
         }

         // Principal creation
         this.userPrincipal = new TestPrincipal (this.username);
         if (!this.subject.getPrincipals ().contains (this.userPrincipal)) {
            this.subject.getPrincipals ().add (this.userPrincipal);
         }
         if (this.debug) {
            System.out.println ("\t\t[TestLoginModule] added TestPrincipal \"" + this.userPrincipal + "\" to Subject");
         }

         cleanState ();
         result = true;
         this.commitSucceeded = true;
      }
      return result;
   }

   /**
    * {@inheritDoc}
    * @see javax.security.auth.spi.LoginModule#abort()
    */
   public boolean abort () throws LoginException
   {
      boolean result = false;
      if (this.succeeded) {
         if (!this.commitSucceeded) {
            this.succeeded = false;
            cleanState ();
            this.userPrincipal = null;
         }
         else {
            logout ();
         }
         result = true;
      }
      return result;
   }

   /**
    * {@inheritDoc}
    * @see javax.security.auth.spi.LoginModule#logout()
    */
   public boolean logout () throws LoginException
   {
      this.subject.getPrincipals ().remove (this.userPrincipal);
      this.succeeded = false;
      this.succeeded = this.commitSucceeded;
      cleanState ();
      this.userPrincipal = null;
      return true;
   }

   /**
    * Cleans the login module state.
    */
   private void cleanState ()
   {
      this.username = null;
      for (int i = 0; i < this.password.length; i++) {
         this.password [i] = ' ';
      }
      this.password = null;
   }

}


Choix du CallbackHandler

Classes Explications
com.sun.security.auth.callback.DialogCallbackHandler
com.sun.security.auth.callback.TextCallbackHandler


Choix des callbacks

Classes Explications
javax.security.auth.callback.ChoiceCallback
javax.security.auth.callback.ConfirmationCallback
javax.security.auth.callback.LanguageCallback
javax.security.auth.callback.NameCallback Classe Callback utilisée pour demander à l'application la saisie d'un nom (par exemple, le nom de l'utilisateur).
javax.security.auth.callback.PasswordCallback Classe Callback utilisée pour demander à l'application la saisie d'un mot de passe.
javax.security.auth.callback.TextInputCallback Classe Callback utilisée pour demander à l'application la saisie d'un texte (avec la possibilité d'indiquer un texte par défaut).
javax.security.auth.callback.TextOutputCallback Classe Callback utilisée pour transmettre à l'application un message d'information, d'avertissement ou d'erreur à afficher.
javax.security.sasl.AuthorizeCallback
javax.security.sasl.RealmCallback
javax.security.sasl.RealmChoiceCallback

Liens externes