7.6.5. Sécurité supplémentaire pour l'authentification EJB

Résumé

Par défaut, lorsque vous effectuez un appel distant à un EJB déployé sur le serveur d'applications, la connexion au serveur est authentifiée et toute demande reçue via cette connexion est exécutée en utilisant les informations d'identification qui ont authentifié la connexion. L'authentification au niveau de la connexion dépend des capacités des mécanismes sous-jacents SASL (Simple Authentication and Security Layer). Plutôt que d'écrire des mécanismes SASL personnalisés, vous pouvez ouvrir et authentifier une connexion au serveur, puis ajouter les jetons de sécurité supplémentaires avant d'appeler un EJB. Cette rubrique décrit comment passer des informations supplémentaires sur la connexion existante du client pour l'authentification de l'EJB.

Les exemples de code ci-dessous sont uniquement à des fins de démonstration. Ils ne présentent qu'une approche possible et doivent être personnalisés pour répondre aux besoins précis de l'application. Le mot de passe est échangé par le mécanisme SASL. Si l'authentification DIGEST-MD5 SASL est utilisée, le mot de passe est toujours haché avec difficulté et non clair. Toutefois, les jetons restants, eux, sont clairs. Si ces jetons contiennent des informations sensibles, vous pouvez activer le cryptage pour la connexion.

Procédure 7.13. Information de sécurité pour l'authentification EJB

Pour un token de sécurité supplémentaire de connexion authentifiée, vous devrez créer les 3 composants suivants.
  1. Créer l'intercepteur côté client

    Cet intercepteur doit implémenter org.jboss.ejb.client.EJBClientInterceptor. L'intercepteur doit passer le token de sécurité supplémentaire par le mappage de données contextuelles, par l'intermédiaire d'un appel EJBClientInvocationContext.getContextData(). Voici un exemple de code d'intercepteur côté client qui crée un token de sécurité supplémentaire :
    public class ClientSecurityInterceptor implements EJBClientInterceptor {
    
        public void handleInvocation(EJBClientInvocationContext context) throws Exception {
            Principal currentPrincipal = SecurityActions.securityContextGetPrincipal();
    
            if (currentPrincipal != null) {
                Map<String, Object> contextData = context.getContextData();
                contextData.put(ServerSecurityInterceptor.DELEGATED_USER_KEY, currentPrincipal.getName());
            }
    
            context.sendRequest();
        }
    
        public Object handleInvocationResult(EJBClientInvocationContext context) throws Exception {
            return context.getResult();
        }
    
    }
    
    Pour obtenir des informations sur la façon de connecter l'intercepteur client dans une application, voir Section 7.6.6, « Utiliser un intercepteur côté client dans une application ».
  2. Créer et configurer l'intercepteur du conteneur côté serveur

    Les classes d'intercepteur de conteneur sont de simples Plain Old Java Objects (POJOs). Elles utilisent @javax.annotation.AroundInvoke pour marquer la méthode qui est invoquée lors de l'invocation sur le bean. Pour plus d'informations sur les intercepteurs de conteneur, consulter : Section 7.6.1, « Intercepteurs de conteneurs ».
    1. Créer l'intercepteur de conteneur

      Cet intercepteur récupère le jeton d'authentification de sécurité du contexte et le passe au domaine JAAS (Java Authentication and Autorisation Service) pour vérification. Voici un exemple de code d'intercepteur de conteneur :
      public class ServerSecurityInterceptor {
      
          private static final Logger logger = Logger.getLogger(ServerSecurityInterceptor.class);
      
          static final String DELEGATED_USER_KEY = ServerSecurityInterceptor.class.getName() + ".DelegationUser";
      
          @AroundInvoke
          public Object aroundInvoke(final InvocationContext invocationContext) throws Exception {
              Principal desiredUser = null;
              UserPrincipal connectionUser = null;
      
              Map<String, Object> contextData = invocationContext.getContextData();
              if (contextData.containsKey(DELEGATED_USER_KEY)) {
                  desiredUser = new SimplePrincipal((String) contextData.get(DELEGATED_USER_KEY));
      
                  Collection<Principal> connectionPrincipals = SecurityActions.getConnectionPrincipals();
      
                  if (connectionPrincipals != null) {
                      for (Principal current : connectionPrincipals) {
                          if (current instanceof UserPrincipal) {
                              connectionUser = (UserPrincipal) current;
                              break;
                          }
                      }
      
                  } else {
                      throw new IllegalStateException("Delegation user requested but no user on connection found.");
                  }
              }
      
      
              ContextStateCache stateCache = null;
              try {
                  if (desiredUser != null && connectionUser != null
                      && (desiredUser.getName().equals(connectionUser.getName()) == false)) {
                      // The final part of this check is to verify that the change does actually indicate a change in user.
                      try {
                          // We have been requested to use an authentication token
                          // so now we attempt the switch.
                          stateCache = SecurityActions.pushIdentity(desiredUser, new OuterUserCredential(connectionUser));
                      } catch (Exception e) {
                          logger.error("Failed to switch security context for user", e);
                          // Don't propagate the exception stacktrace back to the client for security reasons
                          throw new EJBAccessException("Unable to attempt switching of user.");
                      }
                  }
      
                  return invocationContext.proceed();
              } finally {
                  // switch back to original context
                  if (stateCache != null) {
                      SecurityActions.popIdentity(stateCache);;
                  }
              }
          }
      
      
    2. Configurer l'intercepteur du conteneur

      Pour plus d'informations sur la façon de configurer les intercepteurs de conteneurs côté serveur, consulter : Section 7.6.3, « Configurer un intercepteur de conteneur ».
  3. Créer le JAAS LoginModule

    Ce module personnalisé exécute l'authentification à l'aide de l'information de la connexion authentifiée existante ainsi qu'à l'aide qu'un jeton de sécurité supplémentaire. Voici un exemple de code réduit qui utilise le jeton de sécurité supplémentaire et qui exécute l'authentification. On peut trouver l'exemple de code complet dans le quickstart ejb-security-interceptors fourni dans JBoss EAP 6.3 ou version supérieure.
    public class DelegationLoginModule extends AbstractServerLoginModule {
    
        private static final String DELEGATION_PROPERTIES = "delegationProperties";
    
        private static final String DEFAULT_DELEGATION_PROPERTIES = "delegation-mapping.properties";
    
        private Properties delegationMappings;
    
        private Principal identity;
    
        @Override
        public void initialize(Subject subject, CallbackHandler callbackHandler, Map<String, ?> sharedState, Map<String, ?> options) {
            addValidOptions(new String[] { DELEGATION_PROPERTIES });
            super.initialize(subject, callbackHandler, sharedState, options);
    
            String propertiesName;
            if (options.containsKey(DELEGATION_PROPERTIES)) {
                propertiesName = (String) options.get(DELEGATION_PROPERTIES);
            } else {
                propertiesName = DEFAULT_DELEGATION_PROPERTIES;
            }
            try {
                delegationMappings = loadProperties(propertiesName);
            } catch (IOException e) {
                throw new IllegalArgumentException(String.format("Unable to load properties '%s'", propertiesName), e);
            }
        }
    
        @SuppressWarnings("unchecked")
        @Override
        public boolean login() throws LoginException {
            if (super.login() == true) {
                log.debug("super.login()==true");
                return true;
            }
    
            // Time to see if this is a delegation request.
            NameCallback ncb = new NameCallback("Username:");
            ObjectCallback ocb = new ObjectCallback("Password:");
    
            try {
                callbackHandler.handle(new Callback[] { ncb, ocb });
            } catch (Exception e) {
                if (e instanceof RuntimeException) {
                    throw (RuntimeException) e;
                }
                return false; // If the CallbackHandler can not handle the required callbacks then no chance.
            }
    
            String name = ncb.getName();
            Object credential = ocb.getCredential();
    
            if (credential instanceof OuterUserCredential) {
                // This credential type will only be seen for a delegation request, if not seen then the request is not for us.
    
                if (delegationAcceptable(name, (OuterUserCredential) credential)) {
    
                    identity = new SimplePrincipal(name);
                    if (getUseFirstPass()) {
                        String userName = identity.getName();
                        if (log.isDebugEnabled())
                            log.debug("Storing username '" + userName + "' and empty password");
                        // Add the username and an empty password to the shared state map
                        sharedState.put("javax.security.auth.login.name", identity);
                        sharedState.put("javax.security.auth.login.password", "");
                    }
                    loginOk = true;
                    return true;
                }
            }
    
            return false; // Attempted login but not successful.
        }
    
    
    
  4. Ajouter le LoginModule personnalisé à la chaîne

    Vous devez ajouter le nouveau LoginModule personnalisé à l'endroit qui convient dans la chaîne pour qu'il soit invoqué dans le bon ordre. Dans cet exemple, le SaslPlusLoginModule doit être mis dans la chaîne avant le LoginModule qui charge les rôles par l'option password-stacking .
    • Configurer l'ordonnancement du LoginModule par le Management CLI

      Ce qui suit est un exemple de commandes de Management CLI qui enchaînent le SaslPlusLoginModule personnalisé avant que le LoginModule RealmDirect définisse l'option password-stacking.
      /subsystem=security/security-domain=quickstart-domain:add(cache-type=default)
      /subsystem=security/security-domain=quickstart-domain/authentication=classic:add
      /subsystem=security/security-domain=quickstart-domain/authentication=classic/login-module=DelegationLoginModule:add(code=org.jboss.as.quickstarts.ejb_security_plus.SaslPlusLoginModule,flag=optional,module-options={password-stacking=useFirstPass})
      /subsystem=security/security-domain=quickstart-domain/authentication=classic/login-module=RealmDirect:add(code=RealmDirect,flag=required,module-options={password-stacking=useFirstPass})
      Pour plus d'informations sur le Management CLI, voir le chapitre intitulé Management Interfaces du guide Administration and Configuration Guide de JBoss EAP 6 qui se trouve sur le portail clients https://access.redhat.com/site/documentation/JBoss_Enterprise_Application_Platform/
    • Configurer l'ordonnancement du LoginModule manuellement

      Ce qui suit est un exemple de XML qui configure l'ordonnancement du LoginModule dans le sous-système de security du fichier de configuration du serveur. Le SaslPlusLoginModule doit précéder le LoginModule RealmDirect pour qu'il puisse vérifier l'utilisateur distant avant que les rôles utilisateurs ne soient chargés et l'option password-stacking définie.
      <security-domain name="quickstart-domain" cache-type="default">
          <authentication>
              <login-module code="org.jboss.as.quickstarts.ejb_security_plus.SaslPlusLoginModule" flag="required">
                  <module-option name="password-stacking" value="useFirstPass"/>
              </login-module>
              <login-module code="RealmDirect" flag="required">
                  <module-option name="password-stacking" value="useFirstPass"/>
              </login-module>
          </authentication>
      </security-domain>
      
  5. Créer le client distant

    Dans l'exemple de code suivant, on assume que le fichier additional-secret.properties auquel accède le JAAS LoginModule ci-dessus contient la propriété suivante :
    quickstartUser=7f5cc521-5061-4a5b-b814-bdc37f021acc
    
    Le code suivant montre comment créer un token de sécurité et comment le définir avant l'appel EJB. Le token secret est codé en dur dans des buts de démonstration uniquement. Ce client se contente d'afficher les résultats sur la console.
    import static org.jboss.as.quickstarts.ejb_security_plus.EJBUtil.lookupSecuredEJB;
    
    public class RemoteClient {
    
        /**
         * @param args
         */
        public static void main(String[] args) throws Exception {
            SimplePrincipal principal = new SimplePrincipal("quickstartUser");
            Object credential = new PasswordPlusCredential("quickstartPwd1!".toCharArray(), "7f5cc521-5061-4a5b-b814-bdc37f021acc");
    
            SecurityActions.securityContextSetPrincipalCredential(principal, credential);
            SecuredEJBRemote secured = lookupSecuredEJB();
    
            System.out.println(secured.getPrincipalInformation());
        }
    }