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");

















