/**
* Licensed to Apereo under one or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information regarding copyright ownership. Apereo
* licenses this file to you 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 the
* following location:
*
* <p>http://www.apache.org/licenses/LICENSE-2.0
*
* <p>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 org.apereo.portal.services;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apereo.portal.AuthorizationException;
import org.apereo.portal.IUserIdentityStore;
import org.apereo.portal.events.IPortalAuthEventFactory;
import org.apereo.portal.properties.PropertiesManager;
import org.apereo.portal.security.IAdditionalDescriptor;
import org.apereo.portal.security.IOpaqueCredentials;
import org.apereo.portal.security.IPerson;
import org.apereo.portal.security.IPrincipal;
import org.apereo.portal.security.ISecurityContext;
import org.apereo.portal.security.PortalSecurityException;
import org.apereo.portal.security.ThreadNamingRequestFilter;
import org.apereo.portal.security.provider.ChainingSecurityContext;
import org.apereo.portal.utils.MovingAverage;
import org.apereo.portal.utils.MovingAverageSample;
import org.apereo.portal.utils.cache.UsernameTaggedCacheEntryPurger;
import org.jasig.services.persondir.IPersonAttributeDao;
import org.jasig.services.persondir.IPersonAttributes;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
/**
* Attempts to authenticate a user and retrieve attributes associated with the user.
*
* hold the tokens used to represent the principal and credential for each security context.
* This version differs in the way the principal and credentials are set (all contexts are set
* up front after evaluating the tokens). See setContextParameters() also. Changes put in to
* allow credentials and principals to be defined and held by each context.
*/
@Service
public class Authentication {
private static final Log log = LogFactory.getLog(Authentication.class);
private static final String BASE_CONTEXT_NAME = "root";
// Metric counters
private static final MovingAverage authenticationTimes = new MovingAverage();
public static MovingAverageSample lastAuthentication = new MovingAverageSample();
private ThreadNamingRequestFilter threadNamingRequestFilter;
private IUserIdentityStore userIdentityStore;
private IPortalAuthEventFactory portalEventFactory;
private IPersonAttributeDao personAttributeDao;
private UsernameTaggedCacheEntryPurger usernameTaggedCacheEntryPurger;
@Autowired private Set<IAuthenticationListener> authenticationListeners;
@Autowired
public void setUsernameTaggedCacheEntryPurger(
UsernameTaggedCacheEntryPurger usernameTaggedCacheEntryPurger) {
this.usernameTaggedCacheEntryPurger = usernameTaggedCacheEntryPurger;
}
@Autowired
public void setThreadNamingRequestFilter(ThreadNamingRequestFilter threadNamingRequestFilter) {
this.threadNamingRequestFilter = threadNamingRequestFilter;
}
@Autowired
public void setPersonAttributeDao(
@Qualifier("personAttributeDao") IPersonAttributeDao personAttributeDao) {
this.personAttributeDao = personAttributeDao;
}
@Autowired
public void setUserIdentityStore(IUserIdentityStore userIdentityStore) {
this.userIdentityStore = userIdentityStore;
}
@Autowired
public void setPortalEventFactory(IPortalAuthEventFactory portalEventFactory) {
this.portalEventFactory = portalEventFactory;
}
/**
* Attempts to authenticate a given IPerson based on a set of principals and credentials
*
* @param principals
* @param credentials
* @param person
* @exception PortalSecurityException
*/
public void authenticate(
HttpServletRequest request,
Map<String, String> principals,
Map<String, String> credentials,
IPerson person)
throws PortalSecurityException {
// Retrieve the security context for the user
final ISecurityContext securityContext = person.getSecurityContext();
//Set the principals and credentials for the security context chain
this.configureSecurityContextChain(
principals, credentials, person, securityContext, BASE_CONTEXT_NAME);
// NOTE: PortalPreAuthenticatedProcessingFilter looks in the security.properties file to
// determine what tokens to look for that represent the principals and
// credentials for each context. It then retrieves the values from the request
// and stores the values in the principals and credentials HashMaps that are
// passed to the Authentication service.
// Attempt to authenticate the user
final long start = System.currentTimeMillis();
securityContext.authenticate();
final long elapsed = System.currentTimeMillis() - start;
// Check to see if the user was authenticated
if (securityContext.isAuthenticated()) {
lastAuthentication = authenticationTimes.add(elapsed); // metric
// Add the authenticated username to the person object
// the login name may have been provided or reset by the security provider
// so this needs to be done after authentication.
final String userName = securityContext.getPrincipal().getUID();
person.setAttribute(IPerson.USERNAME, userName);
if (log.isDebugEnabled()) {
log.debug(
"FINISHED SecurityContext authentication for user '"
+ userName
+ "' in "
+ elapsed
+ "ms #milestone");
}
threadNamingRequestFilter.updateCurrentUsername(userName);
/*
* Clear cached group info for this user.
*
* There seem to be 2 systems in place for this information:
* - The old system based on EntityCachingService
* - The new system based on ehcache
*
* For uPortal 5, we should work to remove the old system.
*/
GroupService.finishedSession(person); // Old system
for (IAuthenticationListener authListener : authenticationListeners) { // New system
authListener.userAuthenticated(person);
}
//Clear all existing cached data about the person
this.usernameTaggedCacheEntryPurger.purgeTaggedCacheEntries(userName);
// Retrieve the additional descriptor from the security context
final IAdditionalDescriptor addInfo =
person.getSecurityContext().getAdditionalDescriptor();
// Process the additional descriptor if one was created
if (addInfo != null) {
// Replace the passed in IPerson with the additional descriptor if the
// additional descriptor is an IPerson object created by the security context
// NOTE: This is not the preferred method, creation of IPerson objects should be
// handled by the PersonManager.
if (addInfo instanceof IPerson) {
final IPerson newPerson = (IPerson) addInfo;
person.setFullName(newPerson.getFullName());
for (final String attributeName : newPerson.getAttributeMap().keySet()) {
person.setAttribute(attributeName, newPerson.getAttribute(attributeName));
}
this.resetEntityIdentifier(person, newPerson);
}
// If the additional descriptor is a map then we can
// simply copy all of these additional attributes into the IPerson
else if (addInfo instanceof Map) {
// Cast the additional descriptor as a Map
final Map<?, ?> additionalAttributes = (Map<?, ?>) addInfo;
// Copy each additional attribute into the person object
for (final Iterator<?> keys = additionalAttributes.keySet().iterator();
keys.hasNext();
) {
// Get a key
final String key = (String) keys.next();
// Set the attribute
person.setAttribute(key, additionalAttributes.get(key));
}
} else if (addInfo
instanceof ChainingSecurityContext.ChainingAdditionalDescriptor) {
// do nothing
} else {
if (log.isWarnEnabled()) {
log.warn(
"Authentication Service received unknown additional descriptor ["
+ addInfo
+ "]");
}
}
}
// Populate the person object using the PersonDirectory if applicable
if (PropertiesManager.getPropertyAsBoolean(
"org.apereo.portal.services.Authentication.usePersonDirectory")) {
// Retrieve all of the attributes associated with the person logging in
final String username = person.getUserName();
final long timestamp = System.currentTimeMillis();
if (log.isDebugEnabled()) {
log.debug(
"STARTING user attribute gathering for user '"
+ userName
+ "' #milestone");
}
final IPersonAttributes personAttributes =
this.personAttributeDao.getPerson(username);
if (log.isDebugEnabled()) {
log.debug(
"FINISHED user attribute gathering for user '"
+ userName
+ "' in "
+ Long.toString(System.currentTimeMillis() - timestamp)
+ "ms #milestone");
}
if (personAttributes != null) {
// attribs may be null. IPersonAttributeDao returns null when it does not recognize a user at all, as
// distinguished from returning an empty Map of attributes when it recognizes a user has having no
// attributes.
person.setAttributes(personAttributes.getAttributes());
}
}
// Make sure the the user's fullname is set
if (person.getFullName() == null) {
// Use portal display name if one exists
if (person.getAttribute("portalDisplayName") != null) {
person.setFullName((String) person.getAttribute("portalDisplayName"));
}
// If not try the eduPerson displayName
else if (person.getAttribute("displayName") != null) {
person.setFullName((String) person.getAttribute("displayName"));
}
// If still no FullName use an unrecognized string
if (person.getFullName() == null) {
person.setFullName(
"Unrecognized person: " + person.getAttribute(IPerson.USERNAME));
}
}
// Find the uPortal userid for this user or flunk authentication if not found
// The template username should actually be derived from directory information.
// The reference implementation sets the uPortalTemplateUserName to the default in
// the portal.properties file.
// A more likely template would be staff or faculty or undergraduate.
final boolean autocreate =
PropertiesManager.getPropertyAsBoolean(
"org.apereo.portal.services.Authentication.autoCreateUsers");
// If we are going to be auto creating accounts then we must find the default template to use
if (autocreate && person.getAttribute("uPortalTemplateUserName") == null) {
final String defaultTemplateUserName =
PropertiesManager.getProperty(
"org.apereo.portal.services.Authentication.defaultTemplateUserName");
person.setAttribute("uPortalTemplateUserName", defaultTemplateUserName);
}
try {
// Attempt to retrieve the UID
final int newUID = this.userIdentityStore.getPortalUID(person, autocreate);
person.setID(newUID);
} catch (final AuthorizationException ae) {
log.error("Exception retrieving ID", ae);
throw new PortalSecurityException(
"Authentication Service: Exception retrieving UID");
}
}
//Publish a login event for the person
this.portalEventFactory.publishLoginEvent(request, this, person);
}
/**
* Reset the entity identifier in the final person object (exit hook)
*
* @param person
* @param newPerson
*/
protected void resetEntityIdentifier(final IPerson person, final IPerson newPerson) {}
/**
* Get the principal and credential for a specific context and store them in the context.
*
* @param principals
* @param credentials
* @param ctxName
* @param securityContext
* @param person
*/
public void setContextParameters(
Map<String, String> principals,
Map<String, String> credentials,
String ctxName,
ISecurityContext securityContext,
IPerson person) {
if (log.isDebugEnabled()) {
final StringBuilder msg = new StringBuilder();
msg.append("Preparing to authenticate; setting parameters for context name '")
.append(ctxName)
.append("', context class '")
.append(securityContext.getClass().getName())
.append("'");
// Display principalTokens...
msg.append("\n\t Available Principal Tokens");
for (final Object o : principals.entrySet()) {
final Map.Entry<?, ?> y = (Map.Entry<?, ?>) o;
msg.append("\n\t\t").append(y.getKey()).append("=").append(y.getValue());
}
// Keep credentialTokens secret, but indicate whether they were provided...
msg.append("\n\t Available Credential Tokens");
for (final Object o : credentials.entrySet()) {
final Map.Entry<?, ?> y = (Map.Entry<?, ?>) o;
final String val = (String) y.getValue();
String valWasSpecified = null;
if (val != null) {
valWasSpecified = val.trim().length() == 0 ? "empty" : "provided";
}
msg.append("\n\t\t").append(y.getKey()).append(" was ").append(valWasSpecified);
}
log.debug(msg.toString());
}
String username = principals.get(ctxName);
String credential = credentials.get(ctxName);
// If username or credential are null, this indicates that the token was not
// set in security.properties. We will then use the value for root.
username = username != null ? username : (String) principals.get(BASE_CONTEXT_NAME);
credential = credential != null ? credential : (String) credentials.get(BASE_CONTEXT_NAME);
if (log.isDebugEnabled()) {
log.debug("Authentication::setContextParameters() username: " + username);
}
// Retrieve and populate an instance of the principal object
final IPrincipal principalInstance = securityContext.getPrincipalInstance();
if (username != null && !username.equals("")) {
principalInstance.setUID(username);
}
// Retrieve and populate an instance of the credentials object
final IOpaqueCredentials credentialsInstance =
securityContext.getOpaqueCredentialsInstance();
if (credentialsInstance != null) {
credentialsInstance.setCredentials(credential);
}
}
/**
* Recurse through the {@link ISecurityContext} chain, setting the credentials for each. TODO
* This functionality should be moved into the {@link
* org.apereo.portal.security.provider.ChainingSecurityContext}.
*
* @param principals
* @param credentials
* @param person
* @param securityContext
* @param baseContextName
* @throws PortalSecurityException
*/
private void configureSecurityContextChain(
final Map<String, String> principals,
final Map<String, String> credentials,
final IPerson person,
final ISecurityContext securityContext,
final String baseContextName)
throws PortalSecurityException {
this.setContextParameters(
principals, credentials, baseContextName, securityContext, person);
// load principals and credentials for the subContexts
for (final Enumeration<String> subCtxNames = securityContext.getSubContextNames();
subCtxNames.hasMoreElements();
) {
final String fullSubCtxName = subCtxNames.nextElement();
//Strip off the base of the name
String localSubCtxName = fullSubCtxName;
if (fullSubCtxName.startsWith(baseContextName + ".")) {
localSubCtxName = localSubCtxName.substring(baseContextName.length() + 1);
}
final ISecurityContext sc = securityContext.getSubContext(localSubCtxName);
this.configureSecurityContextChain(principals, credentials, person, sc, fullSubCtxName);
}
}
}