/* * JBoss, Home of Professional Open Source. * Copyright 2012, Red Hat, Inc., and individual contributors * as indicated by the @author tags. See the copyright.txt file in the * distribution for a full listing of individual contributors. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.picketlink.identity.federation.bindings.tomcat.sp; import org.apache.catalina.Context; import org.apache.catalina.LifecycleException; import org.apache.catalina.Session; import org.apache.catalina.authenticator.Constants; import org.apache.catalina.connector.Request; import org.apache.catalina.connector.Response; import org.apache.catalina.deploy.LoginConfig; import org.jboss.security.audit.AuditLevel; import org.picketlink.identity.federation.bindings.tomcat.sp.holder.ServiceProviderSAMLContext; import org.picketlink.identity.federation.core.ErrorCodes; import org.picketlink.identity.federation.core.audit.PicketLinkAuditEvent; import org.picketlink.identity.federation.core.audit.PicketLinkAuditEventType; import org.picketlink.identity.federation.core.config.AuthPropertyType; import org.picketlink.identity.federation.core.config.KeyProviderType; import org.picketlink.identity.federation.core.exceptions.ConfigurationException; import org.picketlink.identity.federation.core.exceptions.ParsingException; import org.picketlink.identity.federation.core.exceptions.ProcessingException; import org.picketlink.identity.federation.core.interfaces.TrustKeyManager; import org.picketlink.identity.federation.core.interfaces.TrustKeyProcessingException; import org.picketlink.identity.federation.core.saml.v2.exceptions.AssertionExpiredException; import org.picketlink.identity.federation.core.saml.v2.holders.DestinationInfoHolder; import org.picketlink.identity.federation.core.saml.v2.interfaces.SAML2Handler; import org.picketlink.identity.federation.core.saml.v2.interfaces.SAML2HandlerResponse; import org.picketlink.identity.federation.core.saml.v2.util.DocumentUtil; import org.picketlink.identity.federation.core.util.CoreConfigUtil; import org.picketlink.identity.federation.core.util.StringUtil; import org.picketlink.identity.federation.web.constants.GeneralConstants; import org.picketlink.identity.federation.web.core.HTTPContext; import org.picketlink.identity.federation.web.process.ServiceProviderBaseProcessor; import org.picketlink.identity.federation.web.process.ServiceProviderSAMLRequestProcessor; import org.picketlink.identity.federation.web.process.ServiceProviderSAMLResponseProcessor; import org.picketlink.identity.federation.web.util.HTTPRedirectUtil; import org.picketlink.identity.federation.web.util.PostBindingUtil; import org.picketlink.identity.federation.web.util.RedirectBindingUtil; import org.picketlink.identity.federation.web.util.RedirectBindingUtil.RedirectBindingUtilDestHolder; import org.picketlink.identity.federation.web.util.ServerDetector; import org.w3c.dom.Document; import javax.servlet.ServletException; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URL; import java.security.Principal; import java.util.Arrays; import java.util.List; import java.util.Set; import static org.picketlink.identity.federation.core.util.StringUtil.isNotNull; /** * <p> * Abstract class to be extended by Service Provider valves to handle SAML requests and responses. * </p> * * @author <a href="mailto:asaldhan@redhat.com">Anil Saldhana</a> * @author <a href="mailto:psilva@redhat.com">Pedro Silva</a> * */ public abstract class AbstractSPFormAuthenticator extends BaseFormAuthenticator { protected boolean jbossEnv = false; public AbstractSPFormAuthenticator() { super(); ServerDetector detector = new ServerDetector(); jbossEnv = detector.isJboss(); } /* * (non-Javadoc) * * @see org.picketlink.identity.federation.bindings.tomcat.sp.BaseFormAuthenticator#processStart() */ @Override protected void startPicketLink() throws LifecycleException { super.startPicketLink(); initKeyProvider(context); } /** * <p> * Send the request to the IDP. Subclasses should override this method to implement how requests must be sent to the IDP. * </p> * * @param destination idp url * @param samlDocument request or response document * @param relayState * @param response * @param willSendRequest are we sending Request or Response to IDP * @param destinationQueryStringWithSignature used only with Redirect binding and with signature enabled. * @throws ProcessingException * @throws ConfigurationException * @throws IOException */ protected void sendRequestToIDP(String destination, Document samlDocument, String relayState, Response response, boolean willSendRequest, String destinationQueryStringWithSignature) throws ProcessingException, ConfigurationException, IOException { if (isHttpPostBinding()) { sendHttpPostBindingRequest(destination, samlDocument, relayState, response, willSendRequest); } else { sendHttpRedirectRequest(destination, samlDocument, relayState, response, willSendRequest, destinationQueryStringWithSignature); } } /** * <p> * Sends a HTTP Redirect request to the IDP. * </p> * * @param destination * @param relayState * @param response * @param willSendRequest * @param destinationQueryStringWithSignature * @throws IOException * @throws UnsupportedEncodingException * @throws ConfigurationException * @throws ProcessingException */ protected void sendHttpRedirectRequest(String destination, Document samlDocument, String relayState, Response response, boolean willSendRequest, String destinationQueryStringWithSignature) throws IOException, ProcessingException, ConfigurationException { String destinationQueryString = null; // We already have queryString with signature from SAML2SignatureGenerationHandler if (destinationQueryStringWithSignature != null) { destinationQueryString = destinationQueryStringWithSignature; } else { String samlMessage = DocumentUtil.getDocumentAsString(samlDocument); String base64Request = RedirectBindingUtil.deflateBase64URLEncode(samlMessage.getBytes("UTF-8")); destinationQueryString = RedirectBindingUtil.getDestinationQueryString(base64Request, relayState, willSendRequest); } RedirectBindingUtilDestHolder holder = new RedirectBindingUtilDestHolder(); holder.setDestination(destination).setDestinationQueryString(destinationQueryString); HTTPRedirectUtil.sendRedirectForRequestor(RedirectBindingUtil.getDestinationURL(holder), response); } /** * <p> * Sends a HTTP POST request to the IDP. * </p> * * @param destination * @param samlDocument * @param relayState * @param response * @param willSendRequest * @throws TrustKeyProcessingException * @throws ProcessingException * @throws IOException * @throws ConfigurationException */ protected void sendHttpPostBindingRequest(String destination, Document samlDocument, String relayState, Response response, boolean willSendRequest) throws ProcessingException, IOException, ConfigurationException { String samlMessage = PostBindingUtil.base64Encode(DocumentUtil.getDocumentAsString(samlDocument)); DestinationInfoHolder destinationHolder = new DestinationInfoHolder(destination, samlMessage, relayState); PostBindingUtil.sendPost(destinationHolder, response, willSendRequest); } /** * <p> * Initialize the KeyProvider configurations. This configurations are to be used during signing and validation of SAML * assertions. * </p> * * @param context * @throws LifecycleException */ protected void initKeyProvider(Context context) throws LifecycleException { if (!doSupportSignature()) { return; } KeyProviderType keyProvider = this.spConfiguration.getKeyProvider(); if (keyProvider == null && doSupportSignature()) throw new LifecycleException(ErrorCodes.NULL_VALUE + "KeyProvider is null for context=" + context.getName()); try { String keyManagerClassName = keyProvider.getClassName(); if (keyManagerClassName == null) throw new RuntimeException(ErrorCodes.NULL_VALUE + "KeyManager class name"); Class<?> clazz = SecurityActions.loadClass(getClass(), keyManagerClassName); if (clazz == null) throw new ClassNotFoundException(ErrorCodes.CLASS_NOT_LOADED + keyManagerClassName); this.keyManager = (TrustKeyManager) clazz.newInstance(); List<AuthPropertyType> authProperties = CoreConfigUtil.getKeyProviderProperties(keyProvider); keyManager.setAuthProperties(authProperties); keyManager.setValidatingAlias(keyProvider.getValidatingAlias()); String identityURL = this.spConfiguration.getIdentityURL(); //Special case when you need X509Data in SignedInfo if(authProperties != null){ for(AuthPropertyType authPropertyType: authProperties){ String key = authPropertyType.getKey(); if(GeneralConstants.X509CERTIFICATE.equals(key)){ //we need X509Certificate in SignedInfo. The value is the alias name keyManager.addAdditionalOption(GeneralConstants.X509CERTIFICATE, authPropertyType.getValue()); break; } } } keyManager.addAdditionalOption(ServiceProviderBaseProcessor.IDP_KEY, new URL(identityURL).getHost()); } catch (Exception e) { logger.trustKeyManagerCreationError(e); throw new LifecycleException(e.getLocalizedMessage()); } logger.trace("Key Provider=" + keyProvider.getClassName()); } /** * Authenticate the request * * @param request * @param response * @param config * @return * @throws IOException * @throws {@link RuntimeException} when the response is not of type catalina response object */ public boolean authenticate(Request request, HttpServletResponse response, LoginConfig config) throws IOException { if (response instanceof Response) { Response catalinaResponse = (Response) response; return authenticate(request, catalinaResponse, config); } throw logger.samlSPResponseNotCatalinaResponseError(response); } /* * (non-Javadoc) * * @see org.apache.catalina.authenticator.FormAuthenticator#authenticate(org.apache.catalina.connector.Request, * org.apache.catalina.connector.Response, org.apache.catalina.deploy.LoginConfig) */ @Override public boolean authenticate(Request request, Response response, LoginConfig loginConfig) throws IOException { try { // needs to be done first, *before* accessing any parameters. super.authenticate(..) gets called to late String characterEncoding = getCharacterEncoding(); if (characterEncoding != null) { request.setCharacterEncoding(characterEncoding); } Session session = request.getSessionInternal(true); // check if this call is resulting from the redirect after successful authentication. // if so, make the authentication successful and continue the original request if (saveRestoreRequest && matchRequest(request)) { logger.trace("Restoring request from session '" + session.getIdInternal() + "'"); Principal savedPrincipal = (Principal)session.getNote(Constants.FORM_PRINCIPAL_NOTE); register (request, response, savedPrincipal, Constants.FORM_METHOD, (String)session.getNote(Constants.SESS_USERNAME_NOTE), (String)session.getNote(Constants.SESS_PASSWORD_NOTE)); // try to restore the original request (including post data, etc...) if (restoreRequest(request, session)) { // success! user is authenticated; continue processing original request logger.trace("Continuing with restored request."); return true; } else { // no saved request found... logger.trace("Restore of original request failed!"); response.sendError(HttpServletResponse.SC_BAD_REQUEST); return false; } } // Eagerly look for Local LogOut boolean localLogout = isLocalLogout(request); if (localLogout) { try { sendToLogoutPage(request, response, session); } catch (ServletException e) { logger.samlLogoutError(e); throw new IOException(e); } return false; } String samlRequest = request.getParameter(GeneralConstants.SAML_REQUEST_KEY); String samlResponse = request.getParameter(GeneralConstants.SAML_RESPONSE_KEY); Principal principal = request.getUserPrincipal(); // If we have already authenticated the user and there is no request from IDP or logout from user if (principal != null && !(isGlobalLogout(request) || isNotNull(samlRequest) || isNotNull(samlResponse))) return true; // General User Request if (!isNotNull(samlRequest) && !isNotNull(samlResponse)) { return generalUserRequest(request, response, loginConfig); } // Handle a SAML Response from IDP if (isNotNull(samlResponse)) { return handleSAMLResponse(request, response, loginConfig); } // Handle SAML Requests from IDP if (isNotNull(samlRequest)) { return handleSAMLRequest(request, response, loginConfig); }// end if return localAuthentication(request, response, loginConfig); } catch (IOException e) { if (StringUtil.isNotNull(spConfiguration.getErrorPage())) { try { request.getRequestDispatcher(spConfiguration.getErrorPage()).forward(request.getRequest(), response); } catch (ServletException e1) { logger.samlErrorPageForwardError(spConfiguration.getErrorPage(), e1); } return false; } else { throw e; } } } /** * <p> * Indicates if the current request is a GlobalLogout request. * </p> * * @param request * @return */ private boolean isGlobalLogout(Request request) { String gloStr = request.getParameter(GeneralConstants.GLOBAL_LOGOUT); return isNotNull(gloStr) && "true".equalsIgnoreCase(gloStr); } /** * <p> * Indicates if the current request is a LocalLogout request. * </p> * * @param request * @return */ private boolean isLocalLogout(Request request) { String lloStr = request.getParameter(GeneralConstants.LOCAL_LOGOUT); return isNotNull(lloStr) && "true".equalsIgnoreCase(lloStr); } /** * Handle the IDP Request * * @param request * @param response * @param loginConfig * @return * @throws IOException */ private boolean handleSAMLRequest(Request request, Response response, LoginConfig loginConfig) throws IOException { String samlRequest = request.getParameter(GeneralConstants.SAML_REQUEST_KEY); HTTPContext httpContext = new HTTPContext(request, response, context.getServletContext()); Set<SAML2Handler> handlers = chain.handlers(); try { ServiceProviderSAMLRequestProcessor requestProcessor = new ServiceProviderSAMLRequestProcessor( request.getMethod().equals("POST"), this.serviceURL); requestProcessor.setTrustKeyManager(keyManager); requestProcessor.setConfiguration(spConfiguration); boolean result = requestProcessor.process(samlRequest, httpContext, handlers, chainLock); if (enableAudit) { PicketLinkAuditEvent auditEvent = new PicketLinkAuditEvent(AuditLevel.INFO); auditEvent.setType(PicketLinkAuditEventType.REQUEST_FROM_IDP); auditEvent.setWhoIsAuditing(getContextPath()); auditHelper.audit(auditEvent); } // If response is already commited, we need to stop with processing of HTTP request if (response.isCommitted() || response.isAppCommitted()) return false; if (result) return result; } catch (Exception e) { logger.samlSPHandleRequestError(e); throw logger.samlSPProcessingExceptionError(e); } return localAuthentication(request, response, loginConfig); } /** * Handle IDP Response * * @param request * @param response * @param loginConfig * @return * @throws IOException */ private boolean handleSAMLResponse(Request request, Response response, LoginConfig loginConfig) throws IOException { Session session = request.getSessionInternal(true); String samlResponse = request.getParameter(GeneralConstants.SAML_RESPONSE_KEY); boolean willSendRequest = false; HTTPContext httpContext = new HTTPContext(request, response, context.getServletContext()); Set<SAML2Handler> handlers = chain.handlers(); Principal principal = request.getUserPrincipal(); if (!super.validate(request)) { throw new IOException(ErrorCodes.VALIDATION_CHECK_FAILED); } // deal with SAML response from IDP try { ServiceProviderSAMLResponseProcessor responseProcessor = new ServiceProviderSAMLResponseProcessor(request.getMethod().equals("POST"), serviceURL); responseProcessor.setConfiguration(spConfiguration); if(auditHelper != null){ responseProcessor.setAuditHelper(auditHelper); } responseProcessor.setTrustKeyManager(keyManager); SAML2HandlerResponse saml2HandlerResponse = responseProcessor.process(samlResponse, httpContext, handlers, chainLock); Document samlResponseDocument = saml2HandlerResponse.getResultingDocument(); String relayState = saml2HandlerResponse.getRelayState(); String destination = saml2HandlerResponse.getDestination(); willSendRequest = saml2HandlerResponse.getSendRequest(); String destinationQueryStringWithSignature = saml2HandlerResponse.getDestinationQueryStringWithSignature(); if (destination != null && samlResponseDocument != null) { sendRequestToIDP(destination, samlResponseDocument, relayState, response, willSendRequest, destinationQueryStringWithSignature); } else { // See if the session has been invalidated boolean sessionValidity = session.isValid(); if (!sessionValidity) { sendToLogoutPage(request, response, session); return false; } // We got a response with the principal List<String> roles = saml2HandlerResponse.getRoles(); if (principal == null) principal = (Principal) session.getSession().getAttribute(GeneralConstants.PRINCIPAL_ID); String username = principal.getName(); String password = ServiceProviderSAMLContext.EMPTY_PASSWORD; if (logger.isTraceEnabled()) { logger.trace("Roles determined for username=" + username + "=" + Arrays.toString(roles.toArray())); } // Map to JBoss specific principal if ((new ServerDetector()).isJboss() || jbossEnv) { // Push a context ServiceProviderSAMLContext.push(username, roles); principal = context.getRealm().authenticate(username, password); ServiceProviderSAMLContext.clear(); } else { // tomcat env principal = getGenericPrincipal(request, username, roles); } session.setNote(Constants.SESS_USERNAME_NOTE, username); session.setNote(Constants.SESS_PASSWORD_NOTE, password); request.setUserPrincipal(principal); if (enableAudit) { PicketLinkAuditEvent auditEvent = new PicketLinkAuditEvent(AuditLevel.INFO); auditEvent.setType(PicketLinkAuditEventType.RESPONSE_FROM_IDP); auditEvent.setSubjectName(username); auditEvent.setWhoIsAuditing(getContextPath()); auditHelper.audit(auditEvent); } // Redirect the user to the originally requested URL if (saveRestoreRequest) { // Store the authenticated principal in the session. session.setNote(Constants.FORM_PRINCIPAL_NOTE, principal); // Redirect to the original URL. Note that this will trigger the // authenticator again, but on resubmission we will look in the // session notes to retrieve the authenticated principal and // prevent reauthentication String requestURI = savedRequestURL(session); logger.trace("Redirecting back to original Request URI: " + requestURI); if (requestURI == null) { requestURI = getConfiguration().getServiceURL(); } response.sendRedirect(response.encodeRedirectURL(requestURI)); return false; } register(request, response, principal, Constants.FORM_METHOD, username, password); return true; } } catch (ProcessingException pe) { Throwable t = pe.getCause(); if (t != null && t instanceof AssertionExpiredException) { logger.error("Assertion has expired. Asking IDP for reissue"); if (enableAudit) { PicketLinkAuditEvent auditEvent = new PicketLinkAuditEvent(AuditLevel.INFO); auditEvent.setType(PicketLinkAuditEventType.EXPIRED_ASSERTION); auditEvent.setAssertionID(((AssertionExpiredException) t).getId()); auditHelper.audit(auditEvent); } // Just issue a fresh request back to IDP return generalUserRequest(request, response, loginConfig); } logger.samlSPHandleRequestError(pe); throw logger.samlSPProcessingExceptionError(pe); } catch (Exception e) { logger.samlSPHandleRequestError(e); throw logger.samlSPProcessingExceptionError(e); } return localAuthentication(request, response, loginConfig); } protected boolean isPOSTBindingResponse() { return spConfiguration.isIdpUsesPostBinding(); } /* * (non-Javadoc) * * @see org.picketlink.identity.federation.bindings.tomcat.sp.BaseFormAuthenticator#getBinding() */ @Override protected String getBinding() { return spConfiguration.getBindingType(); } /** * Handle the user invocation for the first time * * @param request * @param response * @param loginConfig * @return * @throws IOException */ private boolean generalUserRequest(Request request, Response response, LoginConfig loginConfig) throws IOException { Session session = request.getSessionInternal(true); boolean willSendRequest = false; HTTPContext httpContext = new HTTPContext(request, response, context.getServletContext()); Set<SAML2Handler> handlers = chain.handlers(); boolean postBinding = spConfiguration.getBindingType().equals("POST"); // Neither saml request nor response from IDP // So this is a user request SAML2HandlerResponse saml2HandlerResponse = null; try { ServiceProviderBaseProcessor baseProcessor = new ServiceProviderBaseProcessor(postBinding, serviceURL); if (issuerID != null) baseProcessor.setIssuer(issuerID); baseProcessor.setIdentityURL(identityURL); baseProcessor.setAuditHelper(auditHelper); baseProcessor.setConfiguration(this.spConfiguration); saml2HandlerResponse = baseProcessor.process(httpContext, handlers, chainLock); } catch (ProcessingException pe) { logger.samlSPHandleRequestError(pe); throw new RuntimeException(pe); } catch (ParsingException pe) { logger.samlSPHandleRequestError(pe); throw new RuntimeException(pe); } catch (ConfigurationException pe) { logger.samlSPHandleRequestError(pe); throw new RuntimeException(pe); } willSendRequest = saml2HandlerResponse.getSendRequest(); Document samlResponseDocument = saml2HandlerResponse.getResultingDocument(); String relayState = saml2HandlerResponse.getRelayState(); String destination = saml2HandlerResponse.getDestination(); String destinationQueryStringWithSignature = saml2HandlerResponse.getDestinationQueryStringWithSignature(); if (destination != null && samlResponseDocument != null) { try { if (saveRestoreRequest) { this.saveRequest(request, session); } if (enableAudit) { PicketLinkAuditEvent auditEvent = new PicketLinkAuditEvent(AuditLevel.INFO); auditEvent.setType(PicketLinkAuditEventType.REQUEST_TO_IDP); auditEvent.setWhoIsAuditing(getContextPath()); auditHelper.audit(auditEvent); } sendRequestToIDP(destination, samlResponseDocument, relayState, response, willSendRequest, destinationQueryStringWithSignature); return false; } catch (Exception e) { logger.samlSPHandleRequestError(e); throw logger.samlSPProcessingExceptionError(e); } } return localAuthentication(request, response, loginConfig); } /** * <p> * Indicates if the SP is configure with HTTP POST Binding. * </p> * * @return */ protected boolean isHttpPostBinding() { return getBinding().equalsIgnoreCase("POST"); } protected Context getContext() { return (Context) getContainer(); } /** * Subclasses need to return the context path * based on the capability of their servlet api * @return */ protected abstract String getContextPath(); protected Principal getGenericPrincipal(Request request, String username, List<String> roles){ return (new SPUtil()).createGenericPrincipal(request, username, roles); } }