blob: 06712a49cd612ea7c9c0d64bf5e442a0b28b98a7 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF 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
*
* 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 org.apache.lenya.ac.shibboleth;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import org.apache.avalon.framework.activity.Disposable;
import org.apache.avalon.framework.parameters.ParameterException;
import org.apache.avalon.framework.parameters.Parameterizable;
import org.apache.avalon.framework.parameters.Parameters;
import org.apache.avalon.framework.service.ServiceException;
import org.apache.avalon.framework.service.ServiceSelector;
import org.apache.cocoon.environment.Context;
import org.apache.cocoon.environment.ObjectModelHelper;
import org.apache.cocoon.environment.Request;
import org.apache.cocoon.environment.Session;
import org.apache.cocoon.environment.http.HttpEnvironment;
import org.apache.lenya.ac.AccessControlException;
import org.apache.lenya.ac.AccessControllerResolver;
import org.apache.lenya.ac.Accreditable;
import org.apache.lenya.ac.AccreditableManager;
import org.apache.lenya.ac.ErrorHandler;
import org.apache.lenya.ac.Group;
import org.apache.lenya.ac.Identity;
import org.apache.lenya.ac.Message;
import org.apache.lenya.ac.Policy;
import org.apache.lenya.ac.Role;
import org.apache.lenya.ac.UserManager;
import org.apache.lenya.ac.UserReference;
import org.apache.lenya.ac.attr.AttributeManager;
import org.apache.lenya.ac.attr.AttributeRule;
import org.apache.lenya.ac.attr.AttributeSet;
import org.apache.lenya.ac.impl.DefaultAccessController;
import org.apache.lenya.ac.impl.TransientUser;
import org.apache.lenya.ac.impl.UserAuthenticator;
import org.apache.lenya.ac.saml.UserFieldsMapper;
import org.apache.lenya.cms.cocoon.acting.DelegatingAuthorizerAction;
import org.apache.lenya.cms.cocoon.components.context.ContextUtility;
import org.apache.lenya.cms.publication.Proxy;
import org.apache.lenya.cms.publication.Publication;
import org.apache.lenya.cms.publication.PublicationFactory;
import org.apache.lenya.cms.publication.URLInformation;
import org.apache.lenya.util.ServletHelper;
import org.apache.shibboleth.AssertionConsumerService;
import org.apache.shibboleth.AttributeRequestService;
import org.apache.shibboleth.impl.AssertionConsumerServiceImpl;
import org.opensaml.SAMLAttribute;
import org.opensaml.SAMLBrowserProfile.BrowserProfileResponse;
/**
* <p>
* Shibboleth-based authenticator.
* <p>
* <p>
* Configuration:
* </p>
* <ul>
* <li> <code>&lt;redirect-to-wayf&gt;true|false&lt;/redirect-to-wayf&gt;</code> - if the
* application should redirect to the WAYF server instead of the login screen if a resource is
* protected only via group rules </li>
*
* </pre>
*/
public class ShibbolethAuthenticator extends UserAuthenticator implements Parameterizable,
Disposable {
protected static final String PREVIOUSLY_REDIRECTED_USER = ShibbolethAuthenticator.class
.getName()
+ "previouslyRedirectedUser";
/**
* Configuration parameter to determine if the WAYF server should be used for logging in to
* rule-only protected pages.
*/
protected static final String PARAM_REDIRECT_TO_WAYF = "redirect-to-wayf";
/**
* Configuration parameter to determine the attribute translator for this authenticator.
*/
protected static final String PARAM_ATTRIBUTE_SET = "attribute-set";
protected static final String ERROR_MISSING_UID_ATTRIBUTE = "Unable to get unique identifier for subject. "
+ "Make sure you are listed in the metadata.xml "
+ "file and your resources your are trying to access "
+ "are available and your are allowed to see them. (Resourceregistry).";
private boolean redirectToWayf = false;
private String attributeSetHint = null;
private AttributeManager attributeManager;
private AttributeSet attributeSet;
/**
* Authenticates the request. If the request contains the parameters <em>username</em> and
* <em>password</em>, the authentication is delegated to the super class
* {@link UserAuthenticator}. Otherwise, the Shibboleth browser profile request is evaluated.
* @see org.apache.lenya.ac.impl.UserAuthenticator#authenticate(org.apache.lenya.ac.AccreditableManager,
* org.apache.cocoon.environment.Request, org.apache.lenya.ac.ErrorHandler)
*/
public boolean authenticate(AccreditableManager accreditableManager, Request request,
ErrorHandler handler) throws AccessControlException {
String reqUsername = request.getParameter("username");
String reqPassword = request.getParameter("password");
if (reqUsername != null && reqPassword != null) {
return super.authenticate(accreditableManager, request, handler);
} else {
Identity identity = (Identity) request.getSession(false).getAttribute(
Identity.class.getName());
return authenticateShibbolethResponse(accreditableManager, identity, handler, request);
}
}
protected boolean authenticateShibbolethResponse(AccreditableManager accreditableManager,
Identity identity, ErrorHandler handler, Request request) throws AccessControlException {
boolean authenticated = false;
HttpServletRequest req = getHttpServletRequest();
AssertionConsumerService consumerService = null;
AttributeRequestService attrReqService = null;
try {
consumerService = (AssertionConsumerService) this.manager
.lookup(AssertionConsumerService.ROLE);
attrReqService = (AttributeRequestService) this.manager
.lookup(AttributeRequestService.ROLE);
ShibbolethUtil util = new ShibbolethUtil(this.manager);
String host = util.getHostUrl();
BrowserProfileResponse bpResponse = consumerService.processRequest(req, host);
Map attributesMap = attrReqService.requestAttributes(bpResponse);
logAttributesMap(attributesMap);
// fetch unique identifier from attributes
String uniqueId = attrReqService.getUniqueID(attributesMap, bpResponse);
if (uniqueId == null) {
issueError(handler, ERROR_MISSING_UID_ATTRIBUTE);
} else {
UserManager userManager = accreditableManager.getUserManager();
if (userManager.contains(uniqueId)) {
getLogger().error(
"Persistent user with ID [" + uniqueId
+ "] exists, can't create transient user.");
handler.error("Shibboleth authentication error (see logfile for details).");
} else {
TransientUser user = new TransientUser(uniqueId);
passAttributes(user, attributesMap);
updateIdentity(identity, user, userManager);
authenticated = true;
}
}
} catch (Exception e) {
authenticated = false;
handler.error("Shibboleth authentication error: " + e.getMessage()
+ " (see logfile for details)");
getLogger().error("Exception during Shibboleth authentication: ", e);
} finally {
if (consumerService != null) {
this.manager.release(consumerService);
}
if (attrReqService != null) {
this.manager.release(attrReqService);
}
}
return authenticated;
}
protected void logAttributesMap(Map attributesMap) {
if (getLogger().isDebugEnabled()) {
getLogger().debug("Shibboleth attribute map: \n\n" + attributesMap.toString() + "\n\n");
}
}
protected void issueError(ErrorHandler handler, String message) {
getLogger().error(message);
handler.error(message);
}
/**
* Replaces the identity's user with a new user.
* @param identity The identity.
* @param user The new user.
*/
protected void updateIdentity(Identity identity, TransientUser user, UserManager userMgr) {
UserReference oldUser = identity.getUserReference();
if (oldUser != null) {
if (getLogger().isDebugEnabled()) {
getLogger().debug("Removing user [" + oldUser + "] from identity.");
}
identity.removeIdentifiable(oldUser);
}
identity.addIdentifiable(new ShibbolethUserReference(user));
if (getLogger().isDebugEnabled()) {
getLogger().debug("Adding user [" + user + "] to identity.");
}
}
/**
* Passes the attributes from the <em>samlAttributes</em> parameter to the <em>user</em>
* object. The {@link AttributeTranslator} service is used to translate the attributes. The name
* and e-mail attributes are extracted using the {@link UserFieldsMapper}.
* @param user
* @param samlAttributes
* @throws AccessControlException
*/
protected void passAttributes(TransientUser user, Map samlAttributes)
throws AccessControlException {
for (Iterator i = samlAttributes.keySet().iterator(); i.hasNext();) {
String key = (String) i.next();
AttributeSet attrs = getAttributeSet();
String alias = attrs.getAttribute(key).getAlias();
SAMLAttribute attribute = (SAMLAttribute) samlAttributes.get(key);
String[] values = getValues(attribute);
user.setAttributeValues(alias, values);
}
UserFieldsMapper mapper = new UserFieldsMapper(this.manager, samlAttributes);
String firstName = mapper.getFirstName();
String lastName = mapper.getLastName();
if (firstName != null && lastName != null) {
user.setName(firstName + " " + lastName);
}
String eMail = mapper.getEMail();
if (eMail != null) {
user.setEmail(eMail);
}
}
protected String[] getValues(SAMLAttribute attribute) {
List valueList = new ArrayList();
for (Iterator i = attribute.getValues(); i.hasNext();) {
String value = (String) i.next();
valueList.add(value);
}
return (String[]) valueList.toArray(new String[valueList.size()]);
}
/**
* Extracts the <code>HttpServletRequest</code> object from the current Cocoon context.
* @return An <code>HttpServletRequest</code> object.
* @throws AccessControlException
*/
protected HttpServletRequest getHttpServletRequest() throws AccessControlException {
HttpServletRequest req;
ContextUtility contextUtil = null;
try {
contextUtil = (ContextUtility) this.manager.lookup(ContextUtility.ROLE);
Map objectModel = contextUtil.getObjectModel();
req = (HttpServletRequest) objectModel.get(HttpEnvironment.HTTP_REQUEST_OBJECT);
} catch (ServiceException e) {
throw new AccessControlException(e);
} finally {
if (contextUtil != null) {
this.manager.release(contextUtil);
}
}
return req;
}
/**
* <p>
* This method returns the URI which displays the login screen:
* </p>
* <ul>
* <li>If the configuration option {@link #PARAM_REDIRECT_TO_WAYF} is set to <code>true</code>
* and the request points to a page which is only protected by rules, we assume that the
* Shibboleth authentication shall be used and return the URL which redirects to the WAYF
* server.</li>
* <li>Otherwise, the Lenya login usecase URL is returned.</li>
* </ul>
* @return A string.
* @see org.apache.lenya.ac.impl.UserAuthenticator#getLoginUri(org.apache.cocoon.environment.Request)
*/
public String getLoginUri(Request request) {
String loginUri = null;
if (this.redirectToWayf && isOnlyRuleProtected(request)) {
// avoid redirect loop (WAYF->IdP->SP->WAYF) because of failing
// authorization
ShibbolethUserReference user = getLoggedInShibboletUser(request);
if (user != null) {
String userId = user.getId();
String previouslyRedirectedUserId = getPreviouslyRedirectedUser(request);
if (previouslyRedirectedUserId != null && userId.equals(previouslyRedirectedUserId)) {
reportAuthorizationError(request);
loginUri = super.getLoginUri(request);
} else {
setPreviouslyRedirectedUser(request, userId);
}
}
if (loginUri == null) {
loginUri = getWayfLoginUrl(request);
}
} else {
loginUri = super.getLoginUri(request);
}
return loginUri;
}
protected void reportAuthorizationError(Request request) {
Session session = request.getSession();
Message[] messages = (Message[]) session.getAttribute(DelegatingAuthorizerAction.ERRORS);
List messageList = messages == null ? new ArrayList(1) : new ArrayList(Arrays
.asList(messages));
messageList.add(new Message("shibboleth-delete-cookies"));
messages = (Message[]) messageList.toArray(new Message[messageList.size()]);
request.getSession().setAttribute(DelegatingAuthorizerAction.ERRORS, messages);
}
protected void setPreviouslyRedirectedUser(Request request, String userId) {
request.getSession().setAttribute(PREVIOUSLY_REDIRECTED_USER, userId);
}
protected String getPreviouslyRedirectedUser(Request request) {
return (String) request.getSession().getAttribute(PREVIOUSLY_REDIRECTED_USER);
}
/**
* @param request The current request.
* @return A Shibboleth user reference or <code>null</code> if no Shiboleth user is logged in.
*/
protected ShibbolethUserReference getLoggedInShibboletUser(Request request) {
Session session = request.getSession(false);
if (session != null) {
Identity identity = (Identity) session.getAttribute(Identity.class.getName());
if (identity != null) {
UserReference user = identity.getUserReference();
if (user instanceof ShibbolethUserReference) {
return (ShibbolethUserReference) user;
}
}
}
return null;
}
/**
* @param request The current request.
* @return The URI which issues a redirect to the WAYF server.
*/
protected String getWayfLoginUrl(Request request) {
ContextUtility contextUtil = null;
String proxyUrl;
try {
contextUtil = (ContextUtility) this.manager.lookup(ContextUtility.ROLE);
Context context = ObjectModelHelper.getContext(contextUtil.getObjectModel());
String servletContextPath = context.getRealPath("");
String webappUrl = ServletHelper.getWebappURI(request);
URLInformation info = new URLInformation(webappUrl);
String pubId = info.getPublicationId();
Publication pub = PublicationFactory.getPublication(pubId, servletContextPath);
String area = info.getArea();
Proxy proxy = pub.getProxy(area, true);
if (proxy != null) {
String prefix = "/" + pubId + "/" + area;
String areaUrl = webappUrl.substring(prefix.length());
proxyUrl = proxy.getUrl() + areaUrl;
} else {
proxyUrl = request.getContextPath() + webappUrl;
}
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
if (contextUtil != null) {
this.manager.release(contextUtil);
}
}
return proxyUrl + "?lenya.usecase=shibboleth&lenya.step=wayf";
}
/**
* <p>
* Checks if a page is protected only with rules:
* </p>
* <ul>
* <li> If the aggregated policy for the current page contains credentials which assign a role
* to a particular user or IP range, or to a group which contains explicitly assigned members,
* the method returns <code>false</code>. </li>
* <li> Otherwise, the method returns <code>true</code>.
* </ul>
* @param request The request referring to the page.
* @return A boolean value.
*/
protected boolean isOnlyRuleProtected(Request request) {
DefaultAccessController accessController = null;
ServiceSelector selector = null;
AccessControllerResolver resolver = null;
try {
selector = (ServiceSelector) manager.lookup(AccessControllerResolver.ROLE + "Selector");
resolver = (AccessControllerResolver) selector
.select(AccessControllerResolver.DEFAULT_RESOLVER);
String url = ServletHelper.getWebappURI(request);
accessController = (DefaultAccessController) resolver.resolveAccessController(url);
AccreditableManager accMgr = accessController.getAccreditableManager();
Policy policy = accessController.getPolicyManager().getPolicy(accMgr, url);
Role[] roles = accMgr.getRoleManager().getRoles();
Set accreditables = new HashSet();
for (int i = 0; i < roles.length; i++) {
Accreditable[] accrs = policy.getAccreditables(roles[i]);
accreditables.addAll(Arrays.asList(accrs));
}
if (accreditables.isEmpty()) {
return false;
}
for (Iterator i = accreditables.iterator(); i.hasNext();) {
Accreditable accr = (Accreditable) i.next();
if (!(accr instanceof Group)) {
return false;
}
Group group = (Group) accr;
AttributeRule rule = group.getRule();
if (rule == null || rule.getRule().trim().length() == 0 || group.getMembers().length > 0) {
return false;
}
}
return true;
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
if (selector != null) {
if (resolver != null) {
if (accessController != null) {
resolver.release(accessController);
}
selector.release(resolver);
}
manager.release(selector);
}
}
}
/**
* <p>
* This method returns the URL of the protected page, as passed from the identity provider as
* the value of the {@link AssertionConsumerServiceImpl#REQ_PARAM_TARGET} request parameter. If
* the request parameter is missing, the current URL, i.e. the assertion consumer URL, is
* returned.
* </p>
* @see org.apache.lenya.ac.impl.UserAuthenticator#getTargetUri(org.apache.cocoon.environment.Request)
*/
public String getTargetUri(Request request) {
String paramName = AssertionConsumerServiceImpl.REQ_PARAM_TARGET;
String target = request.getParameter(paramName);
if (target == null) {
getLogger().warn(
"Request parameter " + paramName + " is missing, using current URI as target.");
return request.getRequestURI();
} else {
return target;
}
}
public void parameterize(Parameters params) throws ParameterException {
this.redirectToWayf = params.getParameterAsBoolean(PARAM_REDIRECT_TO_WAYF,
this.redirectToWayf);
this.attributeSetHint = params.getParameter(PARAM_ATTRIBUTE_SET, this.attributeSetHint);
}
public AttributeSet getAttributeSet() {
if (this.attributeSet == null) {
if (this.attributeSetHint == null) {
throw new RuntimeException("The parameter 'attribute-set' was not set. "
+ "Please check $WEBAPP_HOME/WEB-INF/cocoon.xconf.");
}
try {
this.attributeManager = (AttributeManager) this.manager.lookup(AttributeManager.ROLE);
this.attributeSet = this.attributeManager.getAttributeSet(this.attributeSetHint);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
return this.attributeSet;
}
public void dispose() {
if (this.attributeManager != null) {
this.manager.release(this.attributeManager);
}
}
}