com.vmware.identity.proxyservice.LogonProcessorImpl.java Source code

Java tutorial

Introduction

Here is the source code for com.vmware.identity.proxyservice.LogonProcessorImpl.java

Source

/*
 *  Copyright (c) 2012-2015 VMware, Inc.  All Rights Reserved.
 *
 *  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 com.vmware.identity.proxyservice;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;

import javax.servlet.RequestDispatcher;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.lang.StringEscapeUtils;
import org.apache.commons.lang.Validate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

import com.vmware.identity.diagnostics.DiagnosticsLoggerFactory;
import com.vmware.identity.diagnostics.IDiagnosticsLogger;
import com.vmware.identity.idm.IDPConfig;
import com.vmware.identity.idm.PrincipalId;
import com.vmware.identity.idm.RelyingParty;
import com.vmware.identity.idm.client.SAMLNames;
import com.vmware.identity.saml.DefaultSamlAuthorityFactory;
import com.vmware.identity.saml.InvalidSignatureException;
import com.vmware.identity.saml.InvalidTokenException;
import com.vmware.identity.saml.SamlAuthorityFactory;
import com.vmware.identity.saml.SignatureAlgorithm;
import com.vmware.identity.saml.SystemException;
import com.vmware.identity.saml.SamlTokenSpec.AuthenticationData.AuthnMethod;
import com.vmware.identity.saml.ServerValidatableSamlToken;
import com.vmware.identity.saml.ServerValidatableSamlToken.SubjectValidation;
import com.vmware.identity.saml.idm.IdmPrincipalAttributesExtractorFactory;
import com.vmware.identity.saml.ServerValidatableSamlTokenFactory;
import com.vmware.identity.saml.TokenValidator;
import com.vmware.identity.samlservice.AuthnRequestState;
import com.vmware.identity.samlservice.DefaultIdmAccessorFactory;
import com.vmware.identity.samlservice.IdmAccessor;
import com.vmware.identity.samlservice.OasisNames;
import com.vmware.identity.samlservice.SAMLResponseSender;
import com.vmware.identity.samlservice.SAMLResponseSenderFactory;
import com.vmware.identity.samlservice.SamlServiceException;
import com.vmware.identity.samlservice.SamlValidator.ValidationResult;
import com.vmware.identity.samlservice.impl.ConfigExtractorFactoryImpl;
import com.vmware.identity.samlservice.impl.SAMLAuthnResponseSenderFactory;
import com.vmware.identity.samlservice.Shared;
import com.vmware.identity.session.Session;
import com.vmware.identity.session.SessionManager;
import com.vmware.identity.websso.client.AuthnData;
import com.vmware.identity.websso.client.LogonProcessorEx;
import com.vmware.identity.websso.client.Message;

/**
 * Proxying service  Logon Processor Implementation
 *
 */
public class LogonProcessorImpl implements LogonProcessorEx {

    @Autowired
    private MessageSource messageSource;

    @Autowired
    private SessionManager sessionManager;

    private static final IDiagnosticsLogger log = DiagnosticsLoggerFactory.getLogger(LogonProcessorImpl.class);

    private static final String RPSelectionParam_RPNameList = "rp_display_name_list";
    private static final String RPSelectionParam_RPEntityIDList = "rp_entity_id_list";
    private static final String RPSelectionJSP = "/WEB-INF/views/chooseRP.jsp";
    private static final String RPSelectionParam_SAMLResponse = "trusted_saml_response";
    private static final String RPSelectionParam_DialogTittle = "tittle";
    private static final String ExternalIDPErrorMessageName = "ExternalIDPErrorMessage";

    private final ServerValidatableSamlTokenFactory tokenFactory = new ServerValidatableSamlTokenFactory();

    private final SamlAuthorityFactory samlAuthFactory = new DefaultSamlAuthorityFactory(
            SignatureAlgorithm.RSA_SHA256, new IdmPrincipalAttributesExtractorFactory(Shared.IDM_HOSTNAME),
            new ConfigExtractorFactoryImpl());

    private AuthnRequestState requestState;

    /*
     * WebSSO client library authn error handler.
     * At failure of authentication by IDP, SSO server should notify the initiating service provider or client browser.
     * @see com.vmware.identity.websso.client.LogonProcessor#authenticationError(com.vmware.identity.websso.client.Message, javax.servlet.http.HttpServletResponse)
     */
    @Override
    public void authenticationError(Message arg0, Locale locale, String tenant, HttpServletRequest request,
            HttpServletResponse response) {

        Validate.notNull(arg0, "arg0");
        Validate.notNull(response, "response");
        try {

            ValidationResult vrExtIdpSsoResponse = retrieveValidationResult(arg0);

            //retrieve current VR
            if (!arg0.isIdpInitiated() && requestState != null) {
                ValidationResult vr = requestState.getValidationResult();
                if (!(null != vr && !vr.isValid())) {
                    requestState.setValidationResult(vrExtIdpSsoResponse);
                }
            } else {
                Validate.notEmpty(tenant, "tenant");
                Validate.notNull(locale, "locale");

            }

            //retrieve browser locale
            if (null == locale) {
                locale = requestState == null ? Locale.getDefault() : requestState.getLocale();
            }

            int responseCode = vrExtIdpSsoResponse.getResponseCode();

            //default code is  HttpServletResponse.SC_UNAUTHORIZED
            if (responseCode == HttpServletResponse.SC_OK) {
                responseCode = HttpServletResponse.SC_UNAUTHORIZED;
            }

            String message = vrExtIdpSsoResponse.getMessage(messageSource, locale);

            if (OasisNames.RESPONDER.equals(vrExtIdpSsoResponse.getStatus())) {
                message = messageSource.getMessage(ExternalIDPErrorMessageName, null, locale) + message;
            }
            response.addHeader(Shared.RESPONSE_ERROR_HEADER, Shared.encodeString(message));
            response.sendError(responseCode, message);
            log.info("External IDP responded with ERROR {} message {}", vrExtIdpSsoResponse.getResponseCode(),
                    message);

        } catch (Exception e) {
            log.error("Caught unexpect exception in processing authentication error {}", e.toString());

            throw new IllegalStateException(e);
        }
    }

    @Override
    public void authenticationError(Message message, HttpServletRequest request, HttpServletResponse response) {
        authenticationError(message, null, null, request, response);
    }

    @Override
    public void authenticationSuccess(Message message, HttpServletRequest request, HttpServletResponse response) {
        authenticationSuccess(message, null, null, request, response);
    }

    /* (non-Javadoc)
     * @see com.vmware.identity.websso.client.LogonProcessor#authenticationSuccess(com.vmware.identity.websso.client.Message, javax.servlet.http.HttpServletResponse)
     *
     * We got a validated external token from client lib.  This function does the following:
     * 1. validate the subject in lotus,
     * 2. create session.
     * 3. a) issue response to requesting SP (in SP-initiated request) or
     *    b) redirect to resource URI (in IDP-Initiated request)
     *
     */
    @Override
    public void authenticationSuccess(Message arg0, Locale locale, String tenant, HttpServletRequest request,
            HttpServletResponse response) {

        try {
            Validate.notNull(arg0, "arg0");
            Validate.notNull(response, "response obj for service provider");
            Validate.notNull(request, "request obj for service provider");
            Validate.notEmpty(tenant, "tenant");

            SAMLResponseSenderFactory responseSenderFactory = new SAMLAuthnResponseSenderFactory();

            IdmAccessor idmAccessor = new DefaultIdmAccessorFactory().getIdmAccessor();
            idmAccessor.setTenant(tenant);

            if (arg0.isIdpInitiated()) {
                log.debug("IDP-Initiated: Begin processing authentication response.");
                Validate.notNull(locale, "locale");

                PrincipalId principal = validateExternalUser(idmAccessor, arg0);
                if (principal == null) {
                    authenticationError(arg0, locale, tenant, request, response);
                    return;
                }

                Session currentSession = Shared.getSession(sessionManager, request, tenant);
                if (currentSession == null) {
                    currentSession = this.sessionManager.createSession(principal, AuthnMethod.ASSERTION,
                            arg0.getSessionIndex(), arg0.getSource());

                    String tenantSessionCookieName = Shared.getTenantSessionCookieName(tenant);
                    log.trace("Setting cookie " + tenantSessionCookieName + " value " + currentSession.getId());

                    Shared.addSessionCookie(tenantSessionCookieName, currentSession.getId(), response);
                }

                String rpID = chooseSignInRPSite(idmAccessor, request, response, locale);

                if (null == rpID) {
                    //skip for this request. There will be a new request with RP selected.
                    log.debug("No Relying party was selected yet. Skip sending the SAML response!");
                } else {
                    SAMLResponseSender responseSender = responseSenderFactory.buildResponseSender(tenant, response,
                            locale, null, //for IDP initiated, no relay state in post Response to SP
                            null, //no request
                            currentSession.getAuthnMethod(), currentSession.getId(),
                            currentSession.getPrincipalId(), messageSource, sessionManager);

                    Document token = responseSender.generateTokenForResponse(rpID);

                    responseSender.sendResponseToRP(rpID, token);
                }

                log.info("IDP-Initiated: End processing authentication response. Session was created.");
            } else {

                log.debug("SP-Initiated: Begin processing authentication response.");
                Validate.notNull(requestState, "requestState");
                locale = requestState.getLocale();
                Validate.notNull(locale, "locale");

                tenant = requestState.getIdmAccessor().getTenant();

                PrincipalId principal = validateExternalUser(idmAccessor, arg0);
                if (principal == null) {
                    authenticationError(arg0, locale, tenant, request, response);
                    return;
                }

                //send response to initiating relying party
                requestState.addResponseHeaders(response); // set response headers
                requestState.setPrincipalId(principal);
                requestState.setAuthnMethod(AuthnMethod.ASSERTION);
                requestState.createSession(arg0.getSessionIndex(), arg0.getSource());

                String tenantSessionCookieName = Shared.getTenantSessionCookieName(tenant);
                log.trace("Setting cookie " + tenantSessionCookieName + " value " + requestState.getSessionId());

                Shared.addSessionCookie(tenantSessionCookieName, requestState.getSessionId(), response);

                SAMLResponseSender responseSender = responseSenderFactory.buildResponseSender(tenant, response,
                        locale, null, //for IDP initiated, no relay state in post Response to SP
                        requestState, AuthnMethod.ASSERTION, requestState.getSessionId(), principal, messageSource,
                        sessionManager);

                String rpID = requestState.getAuthnRequest().getIssuer().getValue();

                Validate.notEmpty(rpID, "No Relying party ID in SAML authentication request!");
                Document token = responseSender.generateTokenForResponse(rpID);
                responseSender.sendResponseToRP(rpID, token);

                log.info("SP-Initiated: End processing  Authentication response. Session was created.");
            }
        } catch (InvalidTokenException f) {
            log.error("Caught InvalidTokenException in validating external token {}", f.toString());
            com.vmware.identity.websso.client.ValidationResult extResponseVr = new com.vmware.identity.websso.client.ValidationResult(
                    HttpServletResponse.SC_NOT_ACCEPTABLE, OasisNames.RESPONDER,
                    "Unable to validate token issued by external IDP");
            arg0.setValidationResult(extResponseVr);
            authenticationError(arg0, locale, tenant, request, response);

        } catch (SamlServiceException e) {
            log.error("Caught Saml Service Exception in creating WebSSO session or issuring token. ", e.toString());
            if (!arg0.isIdpInitiated() && requestState != null) {
                requestState.setValidationResult(new ValidationResult(OasisNames.RESPONDER, e.getMessage()));
            }
            authenticationError(arg0, locale, tenant, request, response); // server
        } catch (IOException g) {
            log.error("Caught IOException in sending out response or redirecting to relying party {}",
                    g.toString());
            internalError(g, locale, tenant, request, response);
        }
    }

    /**
     * Choose a relying party site to send the assertion.
     *  1. Check RelayState
     *  2. Check if the selection is made by user.
     *  3. If not promp user for selection and return null from this function. The user selection of RP will come in as
     *      a new request.
     *
     * @param tenant
     * @param request
     * @param response
     * @param locale
     * @return EntityID string of the selected RP. Null if Choose RP dialog is called.
     * @throws SamlServiceException not able to dispatch request to RP selection jsp.
     */
    private String chooseSignInRPSite(IdmAccessor idmAccessor, HttpServletRequest request,
            HttpServletResponse response, Locale locale) throws SamlServiceException {

        Validate.notNull(idmAccessor, "idmAccessor");
        Validate.notNull(request, "request");
        Validate.notNull(response, "response");

        String tenant = idmAccessor.getTenant();
        Validate.notNull(tenant, "tenant");

        //Use RelayState if available.
        String rp_selected = request.getParameter(Shared.RELAY_STATE_PARAMETER);
        if (rp_selected != null && !rp_selected.isEmpty()) {
            String rpID;
            try {
                rpID = URLDecoder.decode(rp_selected, "UTF-8");
            } catch (UnsupportedEncodingException e) {
                throw new SamlServiceException("Failed in URL-decoding RelayState: " + rp_selected, e);
            }
            log.debug("IDP_Initiated response has relay state which will be used for"
                    + " identifying Relying Party. RelyingPartyID: " + rpID);
            return rpID;
        }

        //Check "CastleRPSelection" header
        rp_selected = request.getHeader(Shared.RP_SELECTION_ENTITYID);

        if (rp_selected != null && !rp_selected.isEmpty()) {
            log.debug(
                    "IDP_Initiated response has \"CastleRPSelection\" header which will be used for identifying Relying Party.");
            return rp_selected;
        }

        Collection<RelyingParty> rpCollection = idmAccessor.getRelyingParties(tenant);

        //Prepare lists of RP URL and name for the selection dialog
        Iterator<RelyingParty> it = rpCollection.iterator();

        //Skip the Relying party selection if only one RP is registered
        if (rpCollection.size() == 1) {
            return it.next().getUrl();
        }

        List<String> rpEntityIdList = new ArrayList<String>();
        List<String> rpNameList = new ArrayList<String>();
        while (it.hasNext()) {
            RelyingParty rp = it.next();
            rpEntityIdList.add(rp.getUrl());
            rpNameList.add(rp.getName());
        }

        //dispatch selection form
        request.setAttribute(RPSelectionParam_RPNameList, rpNameList);
        request.setAttribute(RPSelectionParam_RPEntityIDList, rpEntityIdList);

        String samlResponseStr = request.getParameter(Shared.SAML_RESPONSE_PARAMETER);
        request.setAttribute(RPSelectionParam_SAMLResponse, StringEscapeUtils.escapeJavaScript(samlResponseStr));
        request.setAttribute(RPSelectionParam_DialogTittle,
                messageSource.getMessage("ChooseService.Title", null, locale));
        RequestDispatcher dispatcher = request.getRequestDispatcher(RPSelectionJSP);
        try {
            dispatcher.forward(request, response);
        } catch (Exception e) {
            throw new SamlServiceException("Failed in dispatch request to RPSelection.jsp", e);
        }

        return null;
    }

    /*
     * Error condition that we are not able notify SP. This error tipically encountered during sending response to SP.
     * So all we can do is send browser error message.
     *
     * (non-Javadoc)
     * @see com.vmware.identity.websso.client.LogonProcessor#internalError(java.lang.Exception, javax.servlet.http.HttpServletRequest)
     */
    @Override
    public void internalError(Exception error, Locale locale, String tenant, HttpServletRequest request,
            HttpServletResponse response) {
        log.error("Caught internalError in authenticating with external IDP! {}", error.toString());
        Validate.notNull(response, "response");
        try {

            int httpErrorCode = HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
            String messageStr = error.getMessage();
            response.addHeader(Shared.RESPONSE_ERROR_HEADER, Shared.encodeString(messageStr));
            response.sendError(httpErrorCode, messageStr);
            log.info("Send client browser error: " + httpErrorCode + ", message: " + messageStr);

        } catch (IOException e) {
            log.error("Caught IOException in sending out response to relying party! {}", e.toString());
        }
    }

    @Override
    public void internalError(Exception internalError, HttpServletRequest request, HttpServletResponse response) {
        internalError(internalError, null, null, request, response);
    }

    /**
     * Validate principal being asserted by IDP. Provision and update JIT user as needed.
     *  1. Validate external token from the response.
     *  2. Provision JIT user if the feature is on and the subject not found.
     *  3. Update JIT user attributes if it is existing JIT user.
     *
     * @param tenant
     * @param arg0
     * @return  PrincipalId or null if not able to link account to the incoming token
     * @throws InvalidSignatureException
     * @throws InvalidTokenException
     * @throws SystemException
     */
    private PrincipalId validateExternalUser(IdmAccessor idmAccessor, Message arg0)
            throws InvalidSignatureException, InvalidTokenException, SystemException {

        Validate.notNull(idmAccessor, "idmAccessor");
        Validate.notNull(arg0, "Message");
        String tenant = idmAccessor.getTenant();
        Validate.notNull(tenant, "tenant");

        AuthnData authnData = (AuthnData) arg0.getMessageData();
        Validate.notNull(authnData, "authnData");

        Element tokenEle = (Element) authnData.getToken()
                .getElementsByTagNameNS(SAMLNames.NS_VAL_SAML_SAML, SAMLNames.ASSERTION).item(0);
        ServerValidatableSamlToken servToken = tokenFactory.parseToken(tokenEle);

        final TokenValidator authnOnlyTokenValidator = samlAuthFactory.createAuthnOnlyTokenValidator(tenant);
        final ServerValidatableSamlToken validatedToken = authnOnlyTokenValidator.validate(servToken);

        Validate.isTrue(validatedToken.isExternal());

        IDPConfig extIdp = getIssuerIDPConfig(arg0, idmAccessor);
        boolean isJitEnabled = extIdp.getJitAttribute();
        PrincipalId subjectUpn = validatedToken.getSubject().subjectUpn();
        boolean isSubjectValidated = subjectUpn != null
                && validatedToken.getSubject().subjectValidation() != SubjectValidation.None;

        if (!isJitEnabled && !isSubjectValidated) {
            log.error("Not able to validate the subject in external token ");
            // Failed in creating our own token. Report authentication error
            com.vmware.identity.websso.client.ValidationResult extResponseVr = new com.vmware.identity.websso.client.ValidationResult(
                    HttpServletResponse.SC_NOT_ACCEPTABLE, OasisNames.REQUEST_DENIED,
                    "Subject validation of external token." + " Unable to validate the subject.");
            arg0.setValidationResult(extResponseVr);
            return null;
        }

        if (isJitEnabled && !isSubjectValidated) {
            try {
                subjectUpn = idmAccessor.createUserAccountJustInTime(servToken.getSubject(), tenant, extIdp);
            } catch (Exception e) {
                log.error("Failure to create a temporary user.", e);
                com.vmware.identity.websso.client.ValidationResult extResponseVr = new com.vmware.identity.websso.client.ValidationResult(
                        HttpServletResponse.SC_UNAUTHORIZED, OasisNames.RESPONDER, null);
                arg0.setValidationResult(extResponseVr);
                return null;
            }
        }

        if (isJitEnabled) {
            // update or add group information based on attribute info in new token
            try {
                idmAccessor.updateJitUserGroups(subjectUpn, tenant, extIdp.getTokenClaimGroupMappings(),
                        authnData.getAttributes());
            } catch (Exception e) {
                log.error("Encountered an error while updating Jit user groups", e);
                com.vmware.identity.websso.client.ValidationResult extResponseVr = new com.vmware.identity.websso.client.ValidationResult(
                        HttpServletResponse.SC_INTERNAL_SERVER_ERROR, OasisNames.RESPONDER, null);
                arg0.setValidationResult(extResponseVr);
                return null;
            }
        }

        return subjectUpn;
    }

    /**
     *
     * @param arg0  Message object from successful validation of SAML response message
     * @return IDPConfig for the issuer of the message, null if not found.
     */
    private IDPConfig getIssuerIDPConfig(Message arg0, IdmAccessor accessor) {

        Validate.notNull(arg0, "arg0");
        Validate.notNull(accessor, "accessor.");

        IDPConfig idpConfig = null;
        Collection<IDPConfig> extIdps = accessor.getExternalIdps();

        Validate.notEmpty(extIdps, "No IDP registration found.");

        for (IDPConfig entry : extIdps) {
            if (entry.getEntityID().equals(arg0.getSource())) {
                idpConfig = entry;
            }
        }

        return idpConfig;
    }

    /**
     * Propagate authentication status to websso client requestState.
     * check IDP side returned status first. If succeeded, check our validation status of the
     * external IDP response.
     * @param arg0 Message object passed in to the error callback.
     */

    private ValidationResult retrieveValidationResult(Message arg0) {
        Validate.notNull(arg0, "Message object");

        com.vmware.identity.websso.client.ValidationResult extResponseVr = arg0.getValidationResult();
        Validate.notNull(extResponseVr);
        ValidationResult vr = null;

        if (arg0.getStatus() != null && !arg0.getStatus().equals(OasisNames.SUCCESS)) {
            //If it is external IDP authentication error, popogated the error back to RP.
            vr = new ValidationResult(extResponseVr.getResponseCode(), arg0.getStatus(), arg0.getSubstatus());

        } else {
            //Else send the validation error to RP
            vr = new ValidationResult(extResponseVr.getResponseCode(), extResponseVr.getStatus(),
                    extResponseVr.getSubstatus());
        }
        return vr;
    }

    /**
     * @return the requestState
     */
    public AuthnRequestState getRequestState() {
        return requestState;
    }

    /**
     * @param requestState the requestState to set
     */
    public void setRequestState(AuthnRequestState requestState) {
        this.requestState = requestState;
    }
}