/*
* Licensed to Jasig under one or more contributor license
* agreements. See the NOTICE file distributed with this work
* for additional information regarding copyright ownership.
* Jasig 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:
*
* 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.jasig.cas.client.jetty;
import org.eclipse.jetty.security.Authenticator;
import org.eclipse.jetty.security.ServerAuthException;
import org.eclipse.jetty.server.Authentication;
import org.eclipse.jetty.util.component.AbstractLifeCycle;
import org.jasig.cas.client.Protocol;
import org.jasig.cas.client.util.CommonUtils;
import org.jasig.cas.client.util.ReflectUtils;
import org.jasig.cas.client.validation.AbstractCasProtocolUrlBasedTicketValidator;
import org.jasig.cas.client.validation.AbstractUrlBasedTicketValidator;
import org.jasig.cas.client.validation.Assertion;
import org.jasig.cas.client.validation.TicketValidator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
/**
* Jetty authenticator component for container-managed CAS authentication.
* <p><em>NOTE:</em> This component does not support CAS gateway mode.</p>
*
* @author Marvin S. Addison
* @since 3.4.2
*/
public class CasAuthenticator extends AbstractLifeCycle implements Authenticator {
/** Name of authentication method provided by this authenticator. */
public static final String AUTH_METHOD = "CAS";
/** Session attribute used to cache CAS authentication data. */
private static final String CACHED_AUTHN_ATTRIBUTE = "org.jasig.cas.client.jetty.Authentication";
/** Logger instance. */
private final Logger logger = LoggerFactory.getLogger(CasAuthenticator.class);
/** Map of tickets to sessions. */
private final ConcurrentMap<String, WeakReference<HttpSession>> sessionMap =
new ConcurrentHashMap<String, WeakReference<HttpSession>>();
/** CAS ticket validator component. */
private TicketValidator ticketValidator;
/** Space-delimited list of server names. */
private String serverNames;
/** CAS principal attribute containing role data. */
private String roleAttribute;
/** URL to /login URI on CAS server. */
private String casServerLoginUrl;
/** Protocol used by ticket validator. */
private Protocol protocol;
/** CAS renew parameter. */
private boolean renew;
/**
* Sets the CAS ticket validator component.
*
* @param ticketValidator Ticket validator, MUST NOT be null.
*/
public void setTicketValidator(final TicketValidator ticketValidator) {
CommonUtils.assertNotNull(ticketValidator, "TicketValidator cannot be null");
if (ticketValidator instanceof AbstractUrlBasedTicketValidator) {
if (ticketValidator instanceof AbstractCasProtocolUrlBasedTicketValidator) {
protocol = Protocol.CAS2;
} else {
protocol = Protocol.SAML11;
}
casServerLoginUrl = ReflectUtils.getField("casServerUrlPrefix", ticketValidator) + "/login";
renew = (Boolean) ReflectUtils.getField("renew", ticketValidator);
} else {
throw new IllegalArgumentException("Unsupported ticket validator " + ticketValidator);
}
this.ticketValidator = ticketValidator;
}
/**
* Sets the names of the server host running Jetty.
*
* @param nameList Space-delimited list of one or more server names, e.g. "www1.example.com www2.example.com".
* MUST NOT be blank.
*/
public void setServerNames(final String nameList) {
CommonUtils.isNotBlank(nameList);
this.serverNames = nameList;
}
/** @return The name of the CAS principal attribute that contains role data. */
public String getRoleAttribute() {
return roleAttribute;
}
/**
* Sets the name of the CAS principal attribute that contains role data.
*
* @param roleAttribute Role attribute name. MUST NOT be blank.
*/
public void setRoleAttribute(final String roleAttribute) {
CommonUtils.isNotBlank(roleAttribute);
this.roleAttribute = roleAttribute;
}
@Override
public void setConfiguration(final AuthConfiguration configuration) {
// Nothing to do
// All configuration must be via CAS-specific setter methods
}
@Override
public String getAuthMethod() {
return AUTH_METHOD;
}
@Override
public void prepareRequest(final ServletRequest request) {
// Nothing to do
}
@Override
public Authentication validateRequest(
final ServletRequest servletRequest, final ServletResponse servletResponse, final boolean mandatory)
throws ServerAuthException {
final HttpServletRequest request = (HttpServletRequest) servletRequest;
final HttpServletResponse response = (HttpServletResponse) servletResponse;
CasAuthentication authentication = fetchCachedAuthentication(request);
if (authentication != null) {
return authentication;
}
final String ticket = request.getParameter(protocol.getArtifactParameterName());
if (ticket != null && mandatory) {
try {
logger.debug("Attempting to validate {}", ticket);
final Assertion assertion = ticketValidator.validate(ticket, serviceUrl(request, response));
logger.info("Successfully authenticated {}", assertion.getPrincipal());
authentication = new CasAuthentication(this, ticket, assertion);
cacheAuthentication(request, authentication);
} catch (Exception e) {
throw new ServerAuthException("CAS ticket validation failed", e);
}
}
if (authentication != null) {
return authentication;
} else if (mandatory) {
redirectToCas(request, response);
return Authentication.SEND_CONTINUE;
}
return Authentication.UNAUTHENTICATED;
}
@Override
public boolean secureResponse(
final ServletRequest request,
final ServletResponse response,
final boolean mandatory,
final Authentication.User user) throws ServerAuthException {
return true;
}
@Override
protected void doStart() throws Exception {
if (ticketValidator == null) {
throw new RuntimeException("TicketValidator cannot be null");
}
if (serverNames == null) {
throw new RuntimeException("ServerNames cannot be null");
}
}
protected void clearCachedAuthentication(final String ticket) {
final WeakReference<HttpSession> sessionRef = sessionMap.remove(ticket);
if (sessionRef != null && sessionRef.get() != null) {
sessionRef.get().removeAttribute(CACHED_AUTHN_ATTRIBUTE);
}
}
private void cacheAuthentication(final HttpServletRequest request, final CasAuthentication authentication) {
final HttpSession session = request.getSession(true);
if (session != null) {
session.setAttribute(CACHED_AUTHN_ATTRIBUTE, authentication);
sessionMap.put(authentication.getTicket(), new WeakReference<HttpSession>(session));
}
}
private CasAuthentication fetchCachedAuthentication(final HttpServletRequest request) {
final HttpSession session = request.getSession(false);
if (session != null) {
return (CasAuthentication) session.getAttribute(CACHED_AUTHN_ATTRIBUTE);
}
return null;
}
private String serviceUrl(final HttpServletRequest request, final HttpServletResponse response) {
return CommonUtils.constructServiceUrl(
request,
response,
null,
serverNames,
protocol.getServiceParameterName(),
protocol.getArtifactParameterName(),
true);
}
private void redirectToCas(
final HttpServletRequest request, final HttpServletResponse response) throws ServerAuthException {
try {
final String redirectUrl = CommonUtils.constructRedirectUrl(
casServerLoginUrl, protocol.getServiceParameterName(), serviceUrl(request, response), renew, false);
logger.debug("Redirecting to {}", redirectUrl);
response.sendRedirect(redirectUrl);
} catch (IOException e) {
logger.debug("Redirect to CAS failed with error: {}", e);
throw new ServerAuthException("Redirect to CAS failed", e);
}
}
}