org.betaconceptframework.astroboa.engine.service.security.AstroboaLogin.java Source code

Java tutorial

Introduction

Here is the source code for org.betaconceptframework.astroboa.engine.service.security.AstroboaLogin.java

Source

/*
 * Copyright (C) 2005-2012 BetaCONCEPT Limited
 *
 * This file is part of Astroboa.
 *
 * Astroboa is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Astroboa is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with Astroboa.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.betaconceptframework.astroboa.engine.service.security;

import java.io.IOException;
import java.security.acl.Group;
import java.util.Hashtable;
import java.util.List;

import javax.naming.Context;
import javax.naming.InitialContext;
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.AccountNotFoundException;
import javax.security.auth.login.FailedLoginException;
import javax.security.auth.login.LoginException;

import org.apache.commons.lang.StringUtils;
import org.betaconceptframework.astroboa.api.model.exception.CmsException;
import org.betaconceptframework.astroboa.api.security.AstroboaPrincipalName;
import org.betaconceptframework.astroboa.api.security.CmsRole;
import org.betaconceptframework.astroboa.api.security.DisplayNamePrincipal;
import org.betaconceptframework.astroboa.api.security.IdentityPrincipal;
import org.betaconceptframework.astroboa.api.security.PersonUserIdPrincipal;
import org.betaconceptframework.astroboa.api.security.management.IdentityStore;
import org.betaconceptframework.astroboa.api.security.management.Person;
import org.betaconceptframework.astroboa.configuration.RepositoryRegistry;
import org.betaconceptframework.astroboa.context.AstroboaClientContextHolder;
import org.betaconceptframework.astroboa.engine.jcr.dao.RepositoryDao;
import org.betaconceptframework.astroboa.security.AstroboaAuthenticationCallback;
import org.betaconceptframework.astroboa.security.CmsGroup;
import org.betaconceptframework.astroboa.security.CmsPrincipal;
import org.betaconceptframework.astroboa.security.CmsRoleAffiliationFactory;
import org.betaconceptframework.astroboa.security.management.CmsPerson;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * This class is responsible for the authentication of a user who wants
 * to connect to an Astroboa repository.
 * 
 * @author Gregory Chomatas (gchomatas@betaconcept.com)
 * @author Savvas Triantafyllou (striantafyllou@betaconcept.com)
 * 
 */
public class AstroboaLogin {

    private final Logger logger = LoggerFactory.getLogger(getClass());

    private IdentityStore identityStore;

    private boolean useExternalIdentity;

    private Person loggedInPerson = null;

    private Subject subject;

    private CallbackHandler callbackHandler;

    private IdentityPrincipal identity;

    private String repositoryId;

    private RepositoryDao repositoryDao;

    private String authenticationTokenForSYSTEMofInternalIdentityStore;

    public AstroboaLogin(CallbackHandler callbackHandler, IdentityStore internalIdentityStore,
            RepositoryDao repositoryDao) {

        this.subject = new Subject();
        this.callbackHandler = callbackHandler;
        this.identity = null;
        this.identityStore = internalIdentityStore;
        this.loggedInPerson = null;
        this.useExternalIdentity = false;
        this.repositoryId = null;
        this.repositoryDao = repositoryDao;

    }

    /**
     * Override login to provide extra checks in case user credentials are
     * correct
     * @return 
     */
    public Subject login() throws LoginException {

        boolean loginIsSuccessful = internalLogin();

        if (loginIsSuccessful == true) {

            if (!loggedInPerson.isEnabled()) {
                throw new AccountNotFoundException(getUsername());
            }

            //Add identity 
            addIdentityPrincipalToSubject();

            //Add PersonUserIdPrincipal to subject
            addPersonUserIdPrincipalToSubject();

            //Add display name principal
            addDisplayNamePrincipalToSubject();

            //Add roles to subject
            addRolesToSubject();

            return subject;
        } else {
            throw new LoginException(getUsername());
        }

    }

    private String getUsername() {
        if (identity != null) {
            return identity.getName();
        }
        return null;
    }

    private boolean internalLogin() throws LoginException {

        String[] credentialsInfo = getAuthenticationInformation();

        String username = credentialsInfo[0];
        String password = credentialsInfo[1];
        String secretKey = credentialsInfo[2];
        String identityStoreLocation = credentialsInfo[3];
        repositoryId = credentialsInfo[4];

        /*
         * Special case user is ANONYMOUS
         */
        if (userIsAnonymous(username)) {
            identity = new IdentityPrincipal(IdentityPrincipal.ANONYMOUS);

            loadAnonymousPerson();

            return true;
        }

        /*
         * User is not ANONYMOUS
         */
        initializeIdentityStore(identityStoreLocation);

        //All went well
        //Authenticate using IdentityStore
        //If no password is provided then check only if user exists 
        //This must be reviewed as we allow anyone to obtain a user's roles
        if (password == null) {
            //User must exist
            if (!getIdentityStore().userExists(username)) {
                throw new FailedLoginException(username);
            }

            //Also to allow login a secret key must be provided and it must match for this user
            if (!RepositoryRegistry.INSTANCE.isSecretKeyValidForUser(repositoryId, username, secretKey)) {
                throw new FailedLoginException(username);
            }

        } else if (!getIdentityStore().authenticate(username, password)) {
            throw new FailedLoginException(username);
        }

        loadPersonByUserName(username);

        //Identity is username as returned by Identity Store
        //Since username may be case insensitive we do not keep value entered by
        //user but rather we keep value returned from IdentityStore
        //which is responsible to provide username entered during user
        //registration   
        //identity = new IdentityPrincipal(username);
        if (loggedInPerson == null || StringUtils.isBlank(loggedInPerson.getUsername())) {
            throw new FailedLoginException(
                    "No username returned from IdentityStore during login of user " + username);
        }

        identity = new IdentityPrincipal(loggedInPerson.getUsername());

        return true;
    }

    private void initializeIdentityStore(String identityStoreLocation) throws FailedLoginException {

        if (StringUtils.isBlank(identityStoreLocation)) {
            throw new FailedLoginException("No identity store location is provided");
        }

        if (useExternalIdentity) {
            initializeExternalIdentityStore(identityStoreLocation);
        } else {
            setupContextForInternalIdentityStore(identityStoreLocation);
        }

        if (identityStore == null) {
            throw new FailedLoginException(
                    "Could not initialize identity store in location (either a JNDI name or a repository id)"
                            + identityStoreLocation);
        }
    }

    private void loadAnonymousPerson() {

        loggedInPerson = new CmsPerson();
        loggedInPerson.setDisplayName(IdentityPrincipal.ANONYMOUS);
        loggedInPerson.setEnabled(true);
        loggedInPerson.setFamilyName(IdentityPrincipal.ANONYMOUS);
        loggedInPerson.setFatherName(IdentityPrincipal.ANONYMOUS);
        loggedInPerson.setFirstName(IdentityPrincipal.ANONYMOUS);
        loggedInPerson.setUserid(IdentityPrincipal.ANONYMOUS);
        loggedInPerson.setUsername(IdentityPrincipal.ANONYMOUS);

    }

    private boolean userIsAnonymous(String username) {
        return StringUtils.equals(IdentityPrincipal.ANONYMOUS, username);
    }

    private void initializeExternalIdentityStore(String identityStoreLocation) throws FailedLoginException {
        try {
            InitialContext context = new InitialContext();

            //First check to see if initial context has been initiated at all
            Hashtable<?, ?> env = context.getEnvironment();
            String initialContextFactoryName = env != null ? (String) env.get(Context.INITIAL_CONTEXT_FACTORY)
                    : null;

            if (StringUtils.isNotBlank(initialContextFactoryName)) {

                Object serviceReference = context.lookup(identityStoreLocation);

                if (!(serviceReference instanceof IdentityStore)) {
                    if (!identityStoreLocation.endsWith("/local")) {
                        //JNDIName is provided by the user and the object it references is not an instance of IdentityStore.
                        //It is probably an instance of NamingContext which is on top of a local or remote service
                        //Since JNDIName does not end with "/local" , try to locate the local service under the returned NamingContext
                        identityStore = (IdentityStore) context.lookup(identityStoreLocation + "/local");
                    } else {
                        throw new Exception("JNDI Name " + identityStoreLocation
                                + " refers to an object whose type is not IdentityStore. Unable to locate. External Identity Store ");
                    }
                } else {
                    identityStore = (IdentityStore) serviceReference;
                }
                //TODO: It may also be the case another login to the identity store must be done

            } else {
                throw new Exception(
                        "Initial Context Factory Name is blank therefore no initial context is configured, thus any lookup will result in exception."
                                + "External Identity Store " + identityStoreLocation);
            }

        } catch (Exception e) {
            logger.error("", e);
            throw new FailedLoginException("During connection to external Identity Store " + identityStoreLocation);
        }

    }

    private void setupContextForInternalIdentityStore(String identityStoreRepositoryId) {

        //Since we are using the internal identity store, we must setup the security context
        //for the user who will be used to connect to the repository which represents the
        //identity store. This user is the SYSTEM user by default and thus we perform
        //an internal login without the need of the SYSTEM's password
        Subject subject = new Subject();

        //System identity
        subject.getPrincipals().add(new IdentityPrincipal(IdentityPrincipal.SYSTEM));

        //Grant SYSTEM all roles
        Group rolesPrincipal = new CmsGroup(AstroboaPrincipalName.Roles.toString());

        for (CmsRole cmsRole : CmsRole.values()) {
            rolesPrincipal.addMember(new CmsPrincipal(CmsRoleAffiliationFactory.INSTANCE
                    .getCmsRoleAffiliationForRepository(cmsRole, identityStoreRepositoryId)));
        }
        subject.getPrincipals().add(rolesPrincipal);

        //Login using the Subject, the provided roles and SYSTEM's permanent key and get the authentication token
        authenticationTokenForSYSTEMofInternalIdentityStore = repositoryDao.login(identityStoreRepositoryId,
                subject, RepositoryRegistry.INSTANCE.getPermanentKeyForUser(identityStoreRepositoryId,
                        IdentityPrincipal.SYSTEM));

    }

    private void loadPersonByUserName(String username) throws LoginException {

        if (loggedInPerson == null) {
            try {

                loggedInPerson = getIdentityStore().retrieveUser(username);

            } catch (Exception e) {
                logger.error("Problem when loading person for username " + username, e);
                throw new LoginException("Problem when loading person for username " + username);
            }

            if (loggedInPerson == null) {
                throw new AccountNotFoundException(username);
            }

        }
    }

    private void addDisplayNamePrincipalToSubject() {

        if (StringUtils.isNotBlank(loggedInPerson.getDisplayName())) {
            subject.getPrincipals().add(new DisplayNamePrincipal(loggedInPerson.getDisplayName()));
        }
    }

    private void addPersonUserIdPrincipalToSubject() {

        String personUserId = loggedInPerson.getUserid();

        if (StringUtils.isNotBlank(personUserId)) {
            subject.getPrincipals().add(new PersonUserIdPrincipal(personUserId));
        }
    }

    private void addIdentityPrincipalToSubject() {
        subject.getPrincipals().add(identity);
    }

    private void addRolesToSubject() throws LoginException {

        //Must return at list one group named "Roles" in order to be 
        final Group groupContainingAllRoles = new CmsGroup(AstroboaPrincipalName.Roles.toString());

        if (userIsAnonymous(getUsername())) {

            //User ANONYMOUS is a virtual user which must have specific role
            //regardless of whether the identity store is external or internal
            groupContainingAllRoles.addMember(new CmsPrincipal(CmsRoleAffiliationFactory.INSTANCE
                    .getCmsRoleAffiliationForRepository(CmsRole.ROLE_CMS_EXTERNAL_VIEWER, repositoryId)));
        } else {
            List<String> impliedRoles = getIdentityStore().getImpliedRoles(getUsername());

            //Load all roles in a tree
            if (impliedRoles != null) {

                for (String impliedRole : impliedRoles) {
                    groupContainingAllRoles.addMember(new CmsPrincipal(impliedRole));
                }
            }
        }

        subject.getPrincipals().add(groupContainingAllRoles);
    }

    /**
     * 
     * TAKEN FROM Jboss class
     *  
     * org.jboss.security.auth.spi.UsernamePasswordLoginModule
     * 
     * and adjust it to Astroboa requirements
     * 
     * @return
     * @throws LoginException
     */
    private String[] getAuthenticationInformation() throws LoginException {
        String[] info = { null, null, null, null, null };
        // prompt for a username and password
        if (callbackHandler == null) {
            throw new LoginException(
                    "Error: no CallbackHandler available " + "to collect authentication information");
        }

        NameCallback nc = new NameCallback("User name: ", "guest");
        PasswordCallback pc = new PasswordCallback("Password: ", false);
        AstroboaAuthenticationCallback authenticationCallback = new AstroboaAuthenticationCallback(
                "Astroboa authentication info");

        Callback[] callbacks = { nc, pc, authenticationCallback };
        String username = null;
        String password = null;
        String identityStoreLocation = null;
        String userSecretKey = null;
        String repositoryId = null;

        try {
            callbackHandler.handle(callbacks);
            username = nc.getName();
            char[] tmpPassword = pc.getPassword();
            if (tmpPassword != null) {
                char[] credential = new char[tmpPassword.length];
                System.arraycopy(tmpPassword, 0, credential, 0, tmpPassword.length);
                pc.clearPassword();
                password = new String(credential);
            }

            identityStoreLocation = authenticationCallback.getIdentityStoreLocation();

            useExternalIdentity = authenticationCallback.isExternalIdentityStore();

            userSecretKey = authenticationCallback.getSecretKey();

            repositoryId = authenticationCallback.getRepositoryId();
        } catch (IOException e) {
            LoginException le = new LoginException("Failed to get username/password");
            le.initCause(e);
            throw le;
        } catch (UnsupportedCallbackException e) {
            LoginException le = new LoginException("CallbackHandler does not support: " + e.getCallback());
            le.initCause(e);
            throw le;
        }
        info[0] = username;
        info[1] = password;
        info[2] = userSecretKey;
        info[3] = identityStoreLocation;
        info[4] = repositoryId;

        return info;
    }

    private IdentityStore getIdentityStore() {
        if (!useExternalIdentity) {
            if (StringUtils.isBlank(authenticationTokenForSYSTEMofInternalIdentityStore)) {
                throw new CmsException(
                        "Internal Identity Store must be used but there is no authentication token for the SYSTEM user");
            }

            AstroboaClientContextHolder.activateClientContextForAuthenticationToken(
                    authenticationTokenForSYSTEMofInternalIdentityStore);
        }

        return identityStore;
    }
}