/**
* 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.cxf.fediz.was.tai;
import java.io.File;
import java.io.IOException;
import java.net.URLEncoder;
import java.rmi.RemoteException;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import javax.naming.InitialContext;
import javax.security.auth.Subject;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import com.ibm.websphere.security.CustomRegistryException;
import com.ibm.websphere.security.EntryNotFoundException;
import com.ibm.websphere.security.UserRegistry;
import com.ibm.websphere.security.WebTrustAssociationException;
import com.ibm.websphere.security.WebTrustAssociationFailedException;
import com.ibm.wsspi.security.tai.TAIResult;
import com.ibm.wsspi.security.tai.TrustAssociationInterceptor;
import com.ibm.wsspi.security.token.AttributeNameConstants;
import org.apache.cxf.fediz.core.FederationConstants;
import org.apache.cxf.fediz.core.FederationProcessor;
import org.apache.cxf.fediz.core.FederationProcessorImpl;
import org.apache.cxf.fediz.core.FederationRequest;
import org.apache.cxf.fediz.core.FederationResponse;
import org.apache.cxf.fediz.core.config.FederationConfigurator;
import org.apache.cxf.fediz.core.config.FederationContext;
import org.apache.cxf.fediz.core.exception.ProcessingException;
import org.apache.cxf.fediz.was.Constants;
import org.apache.cxf.fediz.was.mapper.DefaultRoleToGroupMapper;
import org.apache.cxf.fediz.was.mapper.RoleToGroupMapper;
import org.apache.cxf.fediz.was.tai.exception.TAIConfigurationException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A Trust Authentication Interceptor (TAI) that trusts a Sign-In-Response
* provided from a configured IP/STS and instantiates
* the corresponding WAS Subject
*/
public class FedizInterceptor implements TrustAssociationInterceptor {
private static final Logger LOG = LoggerFactory.getLogger(FedizInterceptor.class);
private static List<String> authorizedWebApps = new ArrayList<String>(15);
private String configFile;
private FederationConfigurator configurator;
private RoleToGroupMapper mapper;
public String getConfigFile() {
return configFile;
}
public void setConfigFile(String configFile) {
this.configFile = configFile;
}
/*
* (non-Javadoc)
* @see com.ibm.wsspi.security.tai.TrustAssociationInterceptor#cleanup()
*/
@Override
public void cleanup() {
configurator = null;
mapper = null;
}
/*
* (non-Javadoc)
* @see com.ibm.wsspi.security.tai.TrustAssociationInterceptor#getType()
*/
@Override
public String getType() {
return this.getClass().getName();
}
/*
* (non-Javadoc)
* @see com.ibm.wsspi.security.tai.TrustAssociationInterceptor#getVersion()
*/
@Override
public String getVersion() {
return Constants.VERSION;
}
/**
* Registers a WebApplication using its contextPath as a key.
* This method must be called by the associated
* security ServletFilter instance of a secured application at initialization time
*
* @param contextPath
*/
public static void registerContext(String contextPath) {
if (LOG.isDebugEnabled()) {
LOG.debug("Registering secured context-path: " + contextPath);
}
authorizedWebApps.add(contextPath);
}
/**
* Deregister a WebApplication using its contextPath as a key.
* This method must be called by the associated
* security ServletFilter instance of a secured application
* in the #destroy() method
*
* @param contextPath
*/
public static void deRegisterContext(String contextPath) {
if (authorizedWebApps.contains(contextPath)) {
if (LOG.isDebugEnabled()) {
LOG.debug("De-registering secured context-path " + contextPath);
}
synchronized (authorizedWebApps) {
authorizedWebApps.remove(contextPath);
}
}
}
/*
* (non-Javadoc)
* @see com.ibm.wsspi.security.tai.TrustAssociationInterceptor#initialize(java.util.Properties)
*/
@Override
public int initialize(Properties props) throws WebTrustAssociationFailedException {
if (props != null) {
try {
String roleGroupMapper = props.getProperty(Constants.ROLE_GROUP_MAPPER);
if (roleGroupMapper != null && !roleGroupMapper.isEmpty()) {
try {
mapper = (RoleToGroupMapper)Class.forName(roleGroupMapper).newInstance();
if (LOG.isDebugEnabled()) {
LOG.debug("Using the " + roleGroupMapper + " mapper class");
}
mapper.initialize(props);
} catch (Exception e) {
throw new TAIConfigurationException(
"Invalid TAI configuration for idpRoleToGroupMapper: "
+ e.getClass().getName() + " "
+ e.getMessage());
}
} else {
mapper = new DefaultRoleToGroupMapper();
if (LOG.isDebugEnabled()) {
LOG.debug("Using the DefaultRoleToGroupMapper mapper class");
}
}
String configFileLocation = props.getProperty(Constants.CONFIGURATION_FILE_PARAMETER);
if (configFileLocation != null) {
if (LOG.isDebugEnabled()) {
LOG.debug("Configuration file location set to " + configFileLocation);
}
File f = new File(configFileLocation);
configurator = new FederationConfigurator();
configurator.loadConfig(f);
if (LOG.isDebugEnabled()) {
LOG.debug("Federation config loaded from path : " + configFileLocation);
}
} else {
throw new WebTrustAssociationFailedException("Missing required initialization parameter "
+ Constants.CONFIGURATION_FILE_PARAMETER);
}
} catch (Throwable t) {
LOG.warn("Failed initializing TAI", t);
return 1;
}
}
return 0;
}
private FederationContext getFederationContext(HttpServletRequest req) {
String contextPath = req.getContextPath();
if (contextPath == null || contextPath.isEmpty()) {
contextPath = "/";
}
return configurator.getFederationContext(contextPath);
}
/*
* (non-Javadoc)
* @see com.ibm.wsspi.security.tai.TrustAssociationInterceptor#isTargetInterceptor(javax.servlet.http.
* HttpServletRequest)
*/
@Override
public boolean isTargetInterceptor(HttpServletRequest req) throws WebTrustAssociationException {
if (LOG.isDebugEnabled()) {
LOG.debug("Request URI: " + req.getRequestURI());
}
FederationContext context = getFederationContext(req);
if (context != null) {
return true;
} else {
LOG.warn("No Federation Context configured for context-path " + req.getContextPath());
}
return false;
}
/*
* (non-Javadoc)
* @see
* com.ibm.wsspi.security.tai.TrustAssociationInterceptor#negotiateValidateandEstablishTrust(javax.servlet
* .http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
*/
@Override
public TAIResult negotiateValidateandEstablishTrust(HttpServletRequest req, HttpServletResponse resp)
throws WebTrustAssociationFailedException {
if (LOG.isDebugEnabled()) {
LOG.debug("Request URI: " + req.getRequestURI());
}
FederationContext fedCtx = getFederationContext(req);
if (fedCtx == null) {
LOG.warn("No Federation Context configured for context-path " + req.getContextPath());
return TAIResult.create(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
}
try {
// looks for the wa parameter as a way to determine the current step
String wa = req.getParameter(FederationConstants.PARAM_ACTION);
if (LOG.isDebugEnabled()) {
LOG.debug("WS-Federation action: " + (wa == null ? "<not set>" : wa));
}
if (wa == null) {
return handleNoWA(req, resp);
} else {
if (FederationConstants.ACTION_SIGNIN.equals(wa)) {
return handleSignIn(req, resp);
} else {
throw new Exception("Unsupported WS-Federation action [" + wa + "]");
}
}
} catch (Exception e) {
LOG.error("Exception occured validating request", e);
throw new WebTrustAssociationFailedException(e.getMessage());
}
}
private TAIResult handleSignIn(HttpServletRequest req, HttpServletResponse resp)
throws ProcessingException, IOException, WebTrustAssociationFailedException, Exception {
if (req.getMethod().equals(Constants.HTTP_POST_METHOD)) {
if (LOG.isDebugEnabled()) {
LOG.debug("Sign-In-Response received");
}
String wresult = req.getParameter(FederationConstants.PARAM_RESULT);
String wctx = req.getParameter(FederationConstants.PARAM_CONTEXT);
if (wresult != null && wctx != null) {
if (LOG.isDebugEnabled()) {
LOG.debug("Validating RSTR...");
}
// process and validate the token
FederationResponse federationResponse = processSigninRequest(req, resp);
if (LOG.isInfoEnabled()) {
LOG.info("RSTR validated successfully");
}
HttpSession session = req.getSession(true);
session.setAttribute(Constants.SECURITY_TOKEN_SESSION_ATTRIBUTE_KEY, federationResponse);
if (LOG.isInfoEnabled()) {
LOG.info("Redirecting request to " + wctx);
}
resp.sendRedirect(wctx);
return TAIResult.create(HttpServletResponse.SC_FOUND);
} else {
throw new Exception("Missing required parameter [wctx or wresult]");
}
} else {
throw new Exception("Incorrect method GET for Sign-In-Response");
}
}
private TAIResult handleNoWA(HttpServletRequest req, HttpServletResponse resp) throws IOException,
WebTrustAssociationFailedException, Exception {
HttpSession session = req.getSession(false);
if (session == null) {
if (LOG.isDebugEnabled()) {
LOG.debug("No session found. Sending a token request");
}
redirectToIdp(req, resp);
return TAIResult.create(HttpServletResponse.SC_FOUND);
} else {
if (LOG.isDebugEnabled()) {
LOG.debug("Session ID is " + session.getId());
}
FederationResponse federationResponse = (FederationResponse)session
.getAttribute(Constants.SECURITY_TOKEN_SESSION_ATTRIBUTE_KEY);
if (federationResponse != null) {
if (LOG.isInfoEnabled()) {
LOG.info("Security Token found in session: " + federationResponse.getUsername());
}
TAIResult result = null;
// check that the target WebApp is properly configured for Token TTL enforcement
if (authorizedWebApps.contains(req.getContextPath())) {
if (LOG.isDebugEnabled()) {
LOG.info("Security Filter properly configured - forwarding subject");
}
// proceed creating the JAAS Subject
List<String> groupsIds = groupIdsFromTokenRoles(federationResponse);
Subject subject = createSubject(federationResponse, groupsIds, session.getId());
result = TAIResult.create(HttpServletResponse.SC_OK, "ignore", subject);
} else {
result = TAIResult.create(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
LOG.warn("No Security Filter configured for " + req.getContextPath());
}
// leave the Session untouched
session.removeAttribute(Constants.SECURITY_TOKEN_SESSION_ATTRIBUTE_KEY);
return result;
} else {
if (LOG.isInfoEnabled()) {
LOG.info("No Subject found in existing session. Redirecting to IDP");
}
redirectToIdp(req, resp);
return TAIResult.create(HttpServletResponse.SC_FOUND);
}
}
}
protected void redirectToIdp(HttpServletRequest request, HttpServletResponse response)
throws IOException, WebTrustAssociationFailedException {
FederationProcessor processor = new FederationProcessorImpl();
String contextName = request.getContextPath();
if (contextName == null || contextName.isEmpty()) {
contextName = "/";
}
FederationContext fedCtx = getFederationContext(request);
String redirectURL = null;
StringBuilder sb = new StringBuilder();
try {
redirectURL = processor.createSignInRequest(request, fedCtx);
if (redirectURL != null) {
sb.append(redirectURL);
}
request.getQueryString();
if (request.getRequestURI() != null && request.getRequestURI().length() > 0) {
sb.append('&').append(FederationConstants.PARAM_CONTEXT).append('=')
.append(URLEncoder.encode(request.getRequestURI(), "UTF-8"));
}
if (request.getQueryString() != null && !request.getQueryString().isEmpty()) {
sb.append('?');
sb.append(URLEncoder.encode(request.getQueryString(), "UTF-8"));
}
if (redirectURL != null) {
response.sendRedirect(sb.toString());
} else {
LOG.error("RedirectUrl is null. Failed to create SignInRequest.");
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
"Failed to create SignInRequest.");
}
} catch (ProcessingException ex) {
LOG.error("Failed to create SignInRequest", ex);
throw new WebTrustAssociationFailedException(ex.getMessage());
}
}
private List<String> groupIdsFromTokenRoles(FederationResponse federationResponse) throws Exception {
InitialContext ctx = new InitialContext();
UserRegistry reg = (UserRegistry)ctx.lookup(Constants.USER_REGISTRY_JNDI_NAME);
List<String> localGroups = mapper.groupsFromRoles(federationResponse.getRoles());
List<String> groupIds = new ArrayList<String>(1);
if (localGroups != null) {
if (LOG.isDebugEnabled()) {
LOG.debug("Converting " + localGroups.size() + " group names to uids");
}
for (String localGroup : localGroups) {
String guid = convertGroupNameToUniqueId(reg, localGroup);
if (LOG.isDebugEnabled()) {
LOG.debug("Group '" + localGroup + "' maps to guid: " + guid);
}
groupIds.add(guid);
}
}
if (LOG.isInfoEnabled()) {
LOG.info("Group list: " + groupIds.toString());
}
return groupIds;
}
/**
* Creates the JAAS Subject so that WAS Runtime will not check the local registry
*
* @param securityName
* @param uniqueid
* @param groups
* @param token
* @return
*/
private Subject createSubject(FederationResponse federationResponse, List<String> groups, String cacheKey) {
String uniqueId = "user:defaultWIMFileBasedRealm/cn=" + federationResponse.getUsername()
+ ",o=defaultWIMFileBasedRealm";
String completeCacheKey = uniqueId + ':' + cacheKey;
// creating the JAAS Subject so that WAS won't do a lookup in the registry
Subject subject = new Subject();
Map<String, Object> map = new Hashtable<String, Object>();
map.put(AttributeNameConstants.WSCREDENTIAL_UNIQUEID, uniqueId);
map.put(AttributeNameConstants.WSCREDENTIAL_SECURITYNAME, federationResponse.getUsername());
map.put(AttributeNameConstants.WSCREDENTIAL_GROUPS, groups);
map.put(AttributeNameConstants.WSCREDENTIAL_CACHE_KEY, completeCacheKey);
// caching the WS-Federation security token for further reuse by the application
map.put(Constants.SUBJECT_TOKEN_KEY, federationResponse);
subject.getPublicCredentials().add(map);
if (LOG.isDebugEnabled()) {
LOG.debug("Subject credentials: " + map.toString());
}
return subject;
}
public FederationResponse processSigninRequest(HttpServletRequest req, HttpServletResponse resp)
throws ProcessingException {
FederationRequest federationRequest = new FederationRequest();
String wa = req.getParameter(FederationConstants.PARAM_ACTION);
String wct = req.getParameter(FederationConstants.PARAM_CURRENT_TIME);
String wresult = req.getParameter(FederationConstants.PARAM_RESULT);
if (LOG.isDebugEnabled()) {
LOG.debug("wa=" + wa);
LOG.debug("wct=" + wct);
LOG.debug("wresult=" + wresult);
}
federationRequest.setWa(wa);
federationRequest.setWct(wct);
federationRequest.setWresult(wresult);
FederationContext fedCtx = getFederationContext(req);
FederationProcessor processor = new FederationProcessorImpl();
return processor.processRequest(federationRequest, fedCtx);
}
/**
* Convenience method for converting a list of group names to their unique group IDs
*
* @param reg
* @param group
* @return
* @throws EntryNotFoundException
* @throws CustomRegistryException
* @throws RemoteException
*/
private String convertGroupNameToUniqueId(UserRegistry reg, String group) throws EntryNotFoundException,
CustomRegistryException, RemoteException {
return reg.getUniqueGroupId(group);
}
}