/* * #! * Ontopia Realm * #- * Copyright (C) 2001 - 2013 The Ontopia Project * #- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * !# */ package net.ontopia.topicmaps.nav2.realm; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.HashMap; import java.util.HashSet; import java.security.Principal; import java.security.MessageDigest; 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.LoginException; import javax.security.auth.spi.LoginModule; import net.ontopia.net.Base64Encoder; import net.ontopia.topicmaps.entry.SharedStoreRegistry; import net.ontopia.topicmaps.entry.TopicMapReferenceIF; import net.ontopia.topicmaps.entry.TopicMapRepositoryIF; import net.ontopia.topicmaps.entry.TopicMaps; import net.ontopia.topicmaps.core.TopicIF; import net.ontopia.topicmaps.core.TopicMapIF; import net.ontopia.topicmaps.core.TopicMapStoreIF; import net.ontopia.topicmaps.core.TMObjectIF; import net.ontopia.topicmaps.nav2.utils.NavigatorUtils; import net.ontopia.topicmaps.nav2.impl.basic.NavigatorApplication; import net.ontopia.topicmaps.query.core.QueryProcessorIF; import net.ontopia.topicmaps.query.core.QueryResultIF; import net.ontopia.topicmaps.query.core.InvalidQueryException; import net.ontopia.topicmaps.query.utils.QueryUtils; import net.ontopia.utils.OntopiaRuntimeException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * INTERNAL: TMLoginModule provides authentication to web applications by * checking user credentials against information stored in a topicmap. */ public class TMLoginModule implements LoginModule { // initialization of logging facility private static Logger log = LoggerFactory.getLogger(TMLoginModule.class.getName()); // state obtained in the initialize() method private Subject subject; private CallbackHandler callbackHandler; private Map<String, ?> sharedState; private Map<String, ?> options; // the authentication status private boolean loginSucceeded; private boolean commitSucceeded; // username, password and hash method private String username; private String password; private String hashMethod; // principals private Principal userPrincipal; private List<RolePrincipal> rolePrincipals; private String jndiname; protected String topicmapId; private String repositoryId; public TMLoginModule() { log.debug("TMLoginModule: constructor"); rolePrincipals = new ArrayList<RolePrincipal>(); } // LoginModule interface methods ... public boolean abort() throws LoginException { if (!loginSucceeded) { return false; } else if (commitSucceeded == false) { // login succeeded but overall authentication failed loginSucceeded = false; username = null; password = null; userPrincipal = null; rolePrincipals.clear(); } else { // overall authentication succeeded and commit succeeded, // but someone else's commit failed logout(); } return true; } /** * Add relevant Principals to the subject. */ public boolean commit() throws LoginException { if (!loginSucceeded) return false; // add user principal if not already exists userPrincipal = new UserPrincipal(username); if (!subject.getPrincipals().contains(userPrincipal)) subject.getPrincipals().add(userPrincipal); // Use a query to find all the RolePrincipals of the user. processRoles(); // Add all the roleprincipals (whenever necessary) to subject. Iterator<RolePrincipal> iter = rolePrincipals.iterator(); while (iter.hasNext()) { Principal rolePrincipal = iter.next(); if (!subject.getPrincipals().contains(rolePrincipal)) subject.getPrincipals().add(rolePrincipal); } log.debug("TMLoginModule: committed"); commitSucceeded = true; // clean out state username = null; password = null; return true; } public void initialize(Subject subject, CallbackHandler callbackHandler, Map<String, ?> sharedState, Map<String, ?> options) { log.debug("TMLoginModule: initialize"); this.subject = subject; this.callbackHandler = callbackHandler; this.sharedState = sharedState; this.options = options; // get options jndiname = (String)options.get("jndi_repository"); if (jndiname == null) jndiname = (String)options.get("jndiname"); topicmapId = (String)options.get("topicmap"); repositoryId = (String)options.get("repository"); if (topicmapId == null) throw new OntopiaRuntimeException("'topicmap' option is not provided to the JAAS module. Check jaas.config file."); hashMethod = (String)options.get("hashmethod"); if (hashMethod == null) hashMethod = "plaintext"; } /** * Prompt the user for username and password, and verify those. */ public boolean login() throws LoginException { log.debug("TMLoginModule: login"); if (callbackHandler == null) throw new LoginException("Error: no CallbackHandler available " + "to garner authentication information from the user"); // prompt for a user name and password NameCallback nameCallback = new NameCallback("user name: "); PasswordCallback passwordCallback = new PasswordCallback("password: ", false); try { callbackHandler.handle(new Callback[] {nameCallback, passwordCallback}); this.username = nameCallback.getName(); char[] charpassword = passwordCallback.getPassword(); password = (charpassword == null ? "" : new String(charpassword)); passwordCallback.clearPassword(); } catch (java.io.IOException ioe) { throw new LoginException(ioe.toString()); } catch (UnsupportedCallbackException uce) { throw new LoginException("Error: " + uce.getCallback() + " not available to garner authentication information " + "from the user"); } // verify the username/password loginSucceeded = verifyUsernamePassword(username, password); return loginSucceeded; } public boolean logout() throws LoginException { // clear out principals subject.getPrincipals().remove(userPrincipal); Iterator<RolePrincipal> iter = rolePrincipals.iterator(); while (iter.hasNext()) { Principal rolePrincipal = iter.next(); if (!subject.getPrincipals().contains(rolePrincipal)) subject.getPrincipals().remove(rolePrincipal); } log.debug("TMLoginModule: logout"); // clean out state loginSucceeded = false; commitSucceeded = false; username = null; password = null; userPrincipal = null; rolePrincipals.clear(); return true; } // ... LoginModule interface methods. private static String getName(TopicIF topic) { return net.ontopia.topicmaps.utils.TopicStringifiers.getDefaultStringifier().toString(topic); } private static String getId(Object that) { if (that instanceof TMObjectIF) return NavigatorUtils.getStableId((TMObjectIF) that); else if (that instanceof TopicMapReferenceIF) return ((TopicMapReferenceIF)that).getId(); else return null; } protected TopicMapIF getTopicMap() { TopicMapStoreIF store; boolean readonly = true; if (jndiname != null) { SharedStoreRegistry ssr = NavigatorApplication.lookupSharedStoreRegistry(jndiname); TopicMapRepositoryIF repository = ssr.getTopicMapRepository(); TopicMapReferenceIF ref = repository.getReferenceByKey(topicmapId); try { store = ref.createStore(readonly); } catch (java.io.IOException e) { throw new OntopiaRuntimeException("Unable to create store for '" + topicmapId + "'", e); } } else { if (repositoryId == null) store = TopicMaps.createStore(topicmapId, readonly); else store = TopicMaps.createStore(topicmapId, readonly, repositoryId); } log.debug("TMLoginModule Initialised Correctly"); return store.getTopicMap(); } public static String hashPassword(String username, String password, String hashMethod) { String encodedPassword; if (hashMethod.equals("base64")) { try { encodedPassword = Base64Encoder.encode(username+password); } catch (Exception e) { throw new OntopiaRuntimeException( "Problem occurred when attempting to hash password", e); } } else if (hashMethod.equals("md5")) { try { MessageDigest messageDigest = MessageDigest.getInstance("MD5"); byte[] digest = messageDigest.digest((username+password).getBytes("ISO-8859-1")); encodedPassword = Base64Encoder.encode(new String(digest, "ISO-8859-1")); } catch (Exception e) { throw new OntopiaRuntimeException( "Problems occurrend when attempting to hash password", e); } } else if (hashMethod.equals("plaintext")) { encodedPassword = password; } else { throw new OntopiaRuntimeException("Invalid password encoding: " + hashMethod); } return encodedPassword; } /** * Query the topicmap for any roles played by the USER. * Create RolePrincipals for each of those roles. */ private void processRoles() { // NOTE: ignoring roles for now TopicMapIF topicMap = getTopicMap(); QueryResultIF queryResult = null; try { QueryProcessorIF queryProcessor = QueryUtils.getQueryProcessor(topicMap); log.info("Processing roles for user '" + username + "'"); String query = "using um for i\"http://psi.ontopia.net/userman/\"" + "select $ROLE, $PRIVILEGE from " + "instance-of($USER, um:user), " + "occurrence($USER, $O1), type($O1, um:username), value($O1, %USERNAME%), " + "um:plays-role($USER : um:user, $ROLE : um:role), " + "{ um:has-privilege($ROLE : um:receiver, $PRIVILEGE : um:privilege) }?"; Map<String, String> params = Collections.singletonMap("USERNAME", username); queryResult = queryProcessor.execute(query, params); Collection<Object> visited = new HashSet<Object>(); while (queryResult.next()) { // register role (aka user-group) TopicIF r = (TopicIF) queryResult.getValue(0); if (!visited.contains(r)) { String rolename = getName(r); if (rolename != null) rolePrincipals.add(new RolePrincipal(rolename)); visited.add(r); log.info("Added role-principal from user-group '" + rolename + "' for user '" + username + "'"); } // register privilege TopicIF p = (TopicIF) queryResult.getValue(1); if (p != null) { if (!visited.contains(p)) { String rolename = getName(p); if (rolename != null) rolePrincipals.add(new RolePrincipal(rolename)); visited.add(p); log.info("Added role-principal from privilege '" + rolename + "' for user '" + username + "'"); } } } // all users have implicit role 'user' log.info("Added implicit role-principal 'user' for user '" + username + "'"); rolePrincipals.add(new RolePrincipal("user")); } catch (InvalidQueryException e) { throw new OntopiaRuntimeException(e); } finally { if (queryResult != null) queryResult.close(); if (topicMap != null) topicMap.getStore().close(); } } private boolean verifyUsernamePassword(String username, String password) { if (username == null || password == null) return false; TopicMapIF topicMap = getTopicMap(); QueryResultIF queryResult = null; try { log.debug("Topic map: " + topicMap); QueryProcessorIF queryProcessor = QueryUtils.getQueryProcessor(topicMap); String query = "using um for i\"http://psi.ontopia.net/userman/\" " + "select $USER from " + "instance-of($USER, um:user), " + "occurrence($USER, $O1), type($O1, um:username), value($O1, %USERNAME%), " + "occurrence($USER, $O2), type($O2, um:password), value($O2, %PASSWORD%)?"; Map<String, String> params = new HashMap<String, String>(2); params.put("USERNAME", username); params.put("PASSWORD", hashPassword(username, password, hashMethod)); queryResult = queryProcessor.execute(query, params); if (queryResult.next()) { TopicIF user = (TopicIF) queryResult.getValue(0); log.info("Authenticated user: " + user); return true; } else { log.info("User '" + username + "' not authenticated"); return false; } } catch (InvalidQueryException e) { throw new OntopiaRuntimeException(e); } finally { if (queryResult != null) queryResult.close(); if (topicMap != null) topicMap.getStore().close(); } } }