Java:JAAS
Sommaire
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 | 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.
|
Sufficient | Le LoginModule n'est pas tenu de réussir.
|
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:
- soit notre traitement peut générer des exceptions: dans ce cas on implémentera l'interface java.security.PrivilegedExceptionAction,
- soit notre traitement ne génère aucune exception: dans ce cas on implémentera l'interface java.security.PrivilegedAction.
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 |