/*
* JBoss, Home of Professional Open Source
*
* Copyright 2013 Red Hat, Inc. and/or its affiliates.
*
* Licensed 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.picketlink.identity.federation.bindings.tomcat.sp;
import org.apache.catalina.Context;
import org.apache.catalina.Lifecycle;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.LifecycleListener;
import org.apache.catalina.Session;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
import org.apache.catalina.util.LifecycleSupport;
import org.apache.catalina.valves.ValveBase;
import org.picketlink.common.PicketLinkLogger;
import org.picketlink.common.PicketLinkLoggerFactory;
import org.picketlink.common.constants.GeneralConstants;
import org.picketlink.common.util.StringUtil;
import org.picketlink.identity.federation.bindings.tomcat.sp.plugins.PropertiesAccountMapProvider;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* PLINK-344: Account Chooser At the Service Provider to enable redirection to the appropriate IDP
*
* @author Anil Saldhana
* @since January 21, 2014
*/
public abstract class AbstractAccountChooserValve extends ValveBase implements Lifecycle {
protected static final PicketLinkLogger logger = PicketLinkLoggerFactory.getLogger();
public static final String ACCOUNT_CHOOSER_COOKIE_NAME = "picketlink.account.name";
public static final String ACCOUNT_PARAMETER = "idp";
public static final String AUTHENTICATING = "AUTHENTICATING";
public static final String STATE = "STATE";
/**
* Domain Name to be used in the cookie that is sent out
*/
protected String domainName;
protected String accountChooserPage = "/accountChooser.html";
protected ConcurrentHashMap<String, String> idpMap = new ConcurrentHashMap<String, String>();
private String accountIDPMapProviderName = PropertiesAccountMapProvider.class.getName();
protected AccountIDPMapProvider accountIDPMapProvider;
/**
* Sets the account chooser cookie expiry. By default, we choose -1 which means
* cookie exists for the remainder of the browser session.
*/
protected int cookieExpiry = -1;
/**
* The lifecycle event support for this component.
*/
protected LifecycleSupport lifecycle = new LifecycleSupport(this);
@Override
public void start() throws LifecycleException {
try {
Class<?> clazz = SecurityActions.loadClass(getClass(), this.accountIDPMapProviderName);
if (clazz == null) {
throw logger.classNotLoadedError(this.accountIDPMapProviderName);
}
accountIDPMapProvider = (AccountIDPMapProvider) clazz.newInstance();
Context context = (Context) getContainer();
accountIDPMapProvider.setServletContext(context.getServletContext());
idpMap.putAll(accountIDPMapProvider.getIDPMap());
} catch (Exception e) {
throw new LifecycleException("Could not start " + getClass().getName() + ".", e);
}
}
@Override
public void stop() throws LifecycleException {
}
@Override
public void removeLifecycleListener(LifecycleListener listener) {
lifecycle.removeLifecycleListener(listener);
}
@Override
public LifecycleListener[] findLifecycleListeners() {
return lifecycle.findLifecycleListeners();
}
@Override
public void addLifecycleListener(LifecycleListener listener) {
lifecycle.addLifecycleListener(listener);
}
/**
* Set the domain name for the cookie to be sent to the browser
* There is no default.
*
* Setting the domain name for the cookie is optional.
*
* @param domainName
*/
public void setDomainName(String domainName) {
this.domainName = domainName;
}
/**
* Set the cookie expiry in seconds.
* Default value is -1
* @param value
*/
public void setCookieExpiry(String value){
try{
int expiry = Integer.parseInt(value);
cookieExpiry = expiry;
}catch(NumberFormatException nfe){
logger.processingError(nfe);
}
}
/**
* Set the fully qualified name of the implementation of
* {@link org.picketlink.identity.federation.bindings.tomcat.sp.AbstractAccountChooserValve.AccountIDPMapProvider}
*
* Default: {@link org.picketlink.identity.federation.bindings.tomcat.sp.plugins.PropertiesAccountMapProvider}
* @param idpMapProviderName
*/
public void setAccountIDPMapProvider(String idpMapProviderName){
this.accountIDPMapProviderName = idpMapProviderName;
}
/**
* Set the name of the html or jsp page that has the accounts for the
* user to choose.
* Default: "/accountChooser.html" is used
*
* @param pageName
*/
public void setAccountChooserPage(String pageName) {
this.accountChooserPage = pageName;
}
@Override
public void invoke(Request request, Response response) throws IOException, ServletException {
Session session = request.getSessionInternal();
if(idpMap.isEmpty()){
idpMap.putAll(accountIDPMapProvider.getIDPMap());
}
String sessionState = (String) session.getNote(STATE);
String idpChosenKey = request.getParameter(ACCOUNT_PARAMETER);
String cookieValue = cookieValue(request);
if (cookieValue != null || AUTHENTICATING.equals(sessionState)) {
if(idpChosenKey != null){
String chosenIDP = idpMap.get(idpChosenKey);
request.setAttribute(org.picketlink.identity.federation.web.constants.GeneralConstants.DESIRED_IDP, chosenIDP);
}
// Case when user is directed to IDP and wants to change the IDP. So he enters the URL again
if (AUTHENTICATING.equals(sessionState) && request.getParameter(GeneralConstants.SAML_RESPONSE_KEY) == null) {
session.removeNote(STATE);
redirectToChosenPage(accountChooserPage, request, response);
return;
}
proceedToAuthentication(request, response, cookieValue);
} else {
if (idpChosenKey != null) {
String chosenIDP = idpMap.get(idpChosenKey);
if (chosenIDP != null) {
request.setAttribute(org.picketlink.identity.federation.web.constants.GeneralConstants.DESIRED_IDP, chosenIDP);
session.setNote(STATE, AUTHENTICATING);
proceedToAuthentication(request, response, idpChosenKey);
}else {
logger.configurationFileMissing(":IDP Mapping");
throw new ServletException();
}
} else {
// redirect to provided html
//saveRequest(request, request.getSessionInternal());
redirectToChosenPage(accountChooserPage,request,response);
return;
}
}
}
/**
* Proceed to the Service Provider Authentication Mechanism
* @param request
* @param response
* @param cookieValue
* @throws IOException
* @throws ServletException
*/
protected void proceedToAuthentication(Request request, Response response, String cookieValue) throws IOException,
ServletException {
Session session = request.getSessionInternal(false);
try {
/*String sessionState = (String) session.getNote(STATE);
// Case when user is directed to IDP and wants to change the IDP. So he enters the URL again
if (AUTHENTICATING.equals(sessionState) && request.getParameter(GeneralConstants.SAML_RESPONSE_KEY) == null) {
session.removeNote(STATE);
redirectToChosenPage(accountConfirmationPage, request, response);
return;
}*/
getNext().invoke(request, response);
} finally {
String state = session != null ? (String) session.getNote(STATE) : null;
//If we are authenticated and registered at the service provider
if (request.getUserPrincipal() != null && StringUtil.isNotNull(state)) {
session.removeNote(STATE);
// Send back a cookie
Context context = (Context) getContainer();
String contextpath = context.getPath();
if(cookieValue == null){
cookieValue = request.getParameter(AbstractAccountChooserValve.ACCOUNT_PARAMETER);
}
Cookie cookie = new Cookie(ACCOUNT_CHOOSER_COOKIE_NAME, cookieValue);
cookie.setPath(contextpath);
cookie.setMaxAge(cookieExpiry);
if(domainName != null){
cookie.setDomain(domainName);
}
response.addCookie(cookie);
}
}
}
/**
* Redirect user to a page
* @param page
* @param request
* @param response
* @throws ServletException
* @throws IOException
*/
protected void redirectToChosenPage(String page, Request request, Response response) throws ServletException, IOException {
Context context = (Context) getContainer();
RequestDispatcher requestDispatcher = context.getServletContext().getRequestDispatcher(page);
if (requestDispatcher != null) {
requestDispatcher.forward(request.getRequest(), response);
}
}
protected String cookieValue(Request request) {
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
String cookieName = cookie.getName();
String cookieDomain = cookie.getDomain();
if (cookieDomain != null && cookieDomain.equalsIgnoreCase(domainName)) {
// Found a cookie with the same domain name
if (ACCOUNT_CHOOSER_COOKIE_NAME.equals(cookieName)) {
// Found cookie
String cookieValue = cookie.getValue();
String chosenIDP = idpMap.get(cookieValue);
if (chosenIDP != null) {
request.setAttribute(org.picketlink.identity.federation.web.constants.GeneralConstants.DESIRED_IDP, chosenIDP);
return cookieValue;
}
}
}else{
if (ACCOUNT_CHOOSER_COOKIE_NAME.equals(cookieName)) {
// Found cookie
String cookieValue = cookie.getValue();
String chosenIDP = idpMap.get(cookieValue);
if (chosenIDP != null) {
request.setAttribute(org.picketlink.identity.federation.web.constants.GeneralConstants.DESIRED_IDP, chosenIDP);
return cookieValue;
}
}
}
}
}
return null;
}
/**
* Save the original request information into our session.
*
* @param request The request to be saved
* @param session The session to contain the saved information
* @throws IOException
*/
protected abstract void saveRequest(Request request, Session session) throws IOException;
/**
* Restore the original request from information stored in our session. If the original request is no longer present
* (because the session timed out), return <code>false</code>; otherwise, return <code>true</code>.
*
* @param request The request to be restored
* @param session The session containing the saved information
*/
protected abstract boolean restoreRequest(Request request, Session session) throws IOException;
/**
* Interface for obtaining the Identity Provider Mapping
*/
public interface AccountIDPMapProvider{
/**
* Set the servlet context for resources on web classpath
* @param servletContext
*/
void setServletContext(ServletContext servletContext);
/**
* Set a {@link java.lang.ClassLoader} for the Provider
* @param classLoader
*/
void setClassLoader(ClassLoader classLoader);
/**
* Get a map of AccountName versus IDP URLs
* @return
*/
Map<String,String> getIDPMap() throws IOException;
}
}