Integrating Magnolia CMS and custom application security

We have an application integrated into Magnolia CMS. and it’s quite natural to have a single way of handling security. Both Magnolia and our application use JAAS for authentication. However magnolia uses JCR repository to store security data, while our application uses database. Since we didn’t want to cause some possible side effects in Magnolia – the following model was applied: users and a limited set of roles is synchronized between application and Magnolia.

Let’s look at how it looks like.

The first step is to edit login.conf file
Initially it looks like:

appRealm {
com.app.security.AppLoginModule required;
};

magnolia {
info.magnolia.jaas.sp.jcr.JCRAuthenticationModule requisite;
info.magnolia.jaas.sp.jcr.JCRAuthorizationModule required;
};

Lets change it to:

appRealm {
com.app.security.AppLoginModule requisite;
};

magnolia {
com.app.security.CustomLoginModule requisite;
info.magnolia.jaas.sp.jcr.JCRAuthorizationModule required;
};

Where com.app.security.CustomLoginModule, is a new class which is inherited from info.magnolia.jaas.sp.jcr.JCRAuthenticationModule. Below is the code which is used to


public class CustomLoginModule extends JCRAuthenticationModule {
  // Cached user data from the database
  UserDto cachedUser;
  // Cached entity
  Entity entity;

  @Override
  protected void initUser() {
    // Get magnolia a chance to load it's own user.
    super.initUser();
    try {      

      // If the user is not available in Magnolia 
      // JCR repository - null is returned.
      if(getUser() != null) {
         // Lets synchronize data from JCR repository 

         // with the data in the app database.
         synchronizeUser(getUser(), User.class);
      }

      // Reading user data from DB
      final InitialContext ic = new InitialContext();
      Object service = ic

        .lookup("com.app.service.users.AuthenticationService");
      final Class authenticationServiceClass 

        = service.getClass();

      final Method authenticationMethod =
        authenticationServiceClass.getMethod("authenticateUser",

        String.class, String.class);

      cachedUser = (UserDto) authenticationMethod.invoke(
        service, name, new String(pswd));

      // In case if the password of the user has been 
      // changed in magnolia - synchronize it.
      if(getUser() == null) {  
        cachedUser.password = new String(pswd);
      }

      // Add required roles into JAAS context
      final RoleListImpl roleListImpl = new RoleListImpl();
      for (String group : cachedUser.groups) {
        roleListImpl.add(group);            
      }

      // In case if the subject was intialized by 
      // the Magnolia - initialize it.
      if(getUser() == null) {
        subject.getPrincipals().add(getEntity());
        subject.getPrincipals().add(roleListImpl);
        subject.getPrincipals().add(new GroupListImpl());

        // Custom user implementation which extends 
        // info.magnolia.cms.security.ExternalUser
        GenesUser genesUser = 

          new GenesUser(subject, cachedUser);
        user = genesUser;
     }
     // Synchronize user stored in Magnolia with 

     // the data from app database.
     synchronizeUser(cachedUser, UserDto.class);

     // Since now the users are synchronized
     // - login programatically into into application.
     new ProgrammaticLogin().login(name, new String(pswd), "app"
       MgnlContext.getWebContext().getRequest(), 

       MgnlContext.getWebContext().getResponse(), true);
    } catch (Exception e) {
      LOGGER.error(e.getMessage());
    }
  }

 // Builds entity object from the cached user.
 public Entity getEntity() {
   if(entity == null) {            
     entity = new EntityImpl();
     entity.addProperty(Entity.NAME, this.cachedUser.username);
     entity.addProperty(Entity.FULL_NAME, 

       this.cachedUser.fullName);
     entity.addProperty(Entity.PASSWORD, new String(this.pswd));
     entity.addProperty(Entity.EMAIL, cachedUser.email);
     entity.addProperty(Entity.ADDRESS_LINE, 

       cachedUser.address);
     for (String group : cachedUser.groups) {
       addRoleName(group);
     }
   }      
   return entity;
 }

  // Synchronizes users of Magnolia and app, 
  // It's a simple reflection call to some
  // other class which is responsible for synchronization.
  private void synchronizeUser(Object user, Class paramClass) {      
    try {
      final Class classData = Class.forName(

        "com.soluter.genes.AccountSynchronizer");
      final Method method 

        = classData.getMethod("synchronizeUser", paramClass);
      method.invoke(null, user);
    } catch (Exception ex) {
      throw new RuntimeException(ex);
    }
  }
}

AccountSynchronizer does two-way synchronization between JCR repository and application database.

public class AccountSynchronizer {
  // Gets data from the magnolia user and outs them into app database.

  public static void synchronizeUser(User user) {
    // Skip user synchronization for the default users
    if (MgnlUserManager.ANONYMOUS_USER.equals(user.getName())
        || MgnlUserManager.SYSTEM_USER.equals(user.getName())) {
      return;
    }
 

    // Reading user.
    final UserService userService 
      = getServiceLocator().getUserService();
    UserDetailsDto userData 

      = userService.getUserDetails(user.getName());
    boolean newUser = userData == null;
    if (newUser) {
      // In case if the user does not exist in app - create it.

      userData = new UserDetailsDto();
    }

    // Synchronizing data and roles.
    userData.password = user.getPassword();
    userData.username = user.getName();
    userData.email = user.getProperty(Entity.EMAIL);

    
    // Saving data.
    try {
      if (newUser) {
        userService.createUser(userData);
      } else {
        userService.saveUser(userData);
      }
    } catch (Exception ex) {
      throw new RuntimeException(ex);
    }
  }




  // Synchronizes Magnolia user with the data from app data base 
  public static void synchronizeUser(UserDto user) {
    try {

      // Reading public branch of the USERS workspace
      final HierarchyManager hierarchyManager 

        = MgnlContext.getSystemContext()
          .getHierarchyManager(ContentRepository.USERS);
      Content content = ContentUtil.getOrCreateContent(

        hierarchyManager.getRoot().getContent("public"),
        user.username, ItemType.USER, true);
      MgnlUser mgnlUser = new MgnlUser(content) {



        // Change behaviour of what should happen in case of 
        // adding role.
        @Override
        public void addRole(String roleName) {
          final HierarchyManager hierarchyManager 

            = MgnlContext.getSystemContext()
            .getHierarchyManager(ContentRepository.USER_ROLES);
          try {

            // Change behaviour of what should 
            // happen in case of adding role
            Content node = ContentUtil
              .getOrCreateContent(this.getUserNode(), 
              "roles", ItemType.CONTENTNODE, true);
            String value = hierarchyManager.getContent(

              "/" + roleName).getUUID();
            HierarchyManager usersHM 

              = MgnlContext.getSystemContext()
               .getHierarchyManager(ContentRepository.USERS);
            String newName = Path.getUniqueLabel(

              usersHM, node.getHandle(), "0");
            // New node is created in repo
            node.createNodeData(newName).setValue(value);
          } catch (Exception e) {
            e.printStackTrace();
          }
        }

        // Change behaviour of what should happen 
        // in case of removing  role.
        @Override
        public void removeRole(String roleName) {
          final HierarchyManager hierarchyManager 

            = MgnlContext.getSystemContext()
              .getHierarchyManager(
                ContentRepository.USER_ROLES);
          try {
            Content node = ContentUtil

              .getOrCreateContent(this.getUserNode(), "roles",
                ItemType.CONTENTNODE, true);
            // Pass through all of the nodes and get 
            // determine node to be deleted
            for (NodeData nodeData : 
              node.getNodeDataCollection()) {
              if (hierarchyManager.getContentByUUID(

                nodeData.getString())
                .getName().equalsIgnoreCase(roleName)) {
                // Node is deleted from repo
                nodeData.delete();
              }
            }
          } catch (Exception e) {
            e.printStackTrace();
          }
        }
      };

      // Syncronizing roles/profile data of the user.
      if (user.password != null) {
        mgnlUser.setProperty(

          MgnlUserManager.PROPERTY_PASSWORD,
          new String(Base64.encodeBase64(user.password.getBytes())));
      }
      mgnlUser.setProperty(MgnlUserManager.PROPERTY_EMAIL,

        user.email);


      // determine roles to be added     
      final List rolesToAdd = ...;    
     
// determine roles to be removed     
      final List rolesToRemove = ...; 
      final List currentRoles = Arrays.asList(user.groups);


      // Remove roles.

      for (String roleName : rolesToRemove) {
        mgnlUser.removeRole(roleName);
      }



      // Add roles.

      for (String roleName : rolesToAdd) {
        mgnlUser.addRole(roleName);
      }

      // Save user data.
      mgnlUser.getUserNode().save();
    } catch (Exception ex) {
      throw new RuntimeException(ex);
    }
  }
}

Since we already have application UI which is used to create roles specific for the application, change user details and password – we call AccountSynchronizer#synchronizeUser every time we update user account.

Also we allow anonymous users to access web site and have a separate login form. To provide logins both into magnolia and application from the form – the following code is used:


// Login into application
new ProgrammaticLogin().login(username, password,
  "app", getThreadLocalRequest(), getThreadLocalResponse(),
  true);

// Login into magnolia
CredentialsCallbackHandler callbackHandler = new PlainTextCallbackHandler(
  username, password.toCharArray(), "public");

SecuritySupport.Factory.getInstance().authenticate(
  callbackHandler, "magnolia");