fi.vm.kapa.identification.service.ServiceProvider.java Source code

Java tutorial

Introduction

Here is the source code for fi.vm.kapa.identification.service.ServiceProvider.java

Source

/**
 * The MIT License
 * Copyright (c) 2015 Population Register Centre
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package fi.vm.kapa.identification.service;

import fi.vm.kapa.identification.client.ProxyClient;
import fi.vm.kapa.identification.dto.ProxyMessageDTO;
import fi.vm.kapa.identification.exception.InternalErrorException;
import fi.vm.kapa.identification.exception.InvalidRequestException;
import fi.vm.kapa.identification.type.Identifier;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.ws.rs.Path;
import javax.ws.rs.core.MultivaluedMap;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

@Component
@Path("/saml")
public class ServiceProvider {

    private static final Logger logger = LoggerFactory.getLogger(ServiceProvider.class);

    private PhaseIdHistoryService historyService;
    private ProxyClient proxyClient;

    @Value("${phase.id.shared.secret}")
    private String sharedSecret;
    @Value("${phase.id.algorithm}")
    private String hmacAlgorithm;
    @Value("${phase.id.time.interval}")
    private int timeInterval;
    @Value("${success.redirect}")
    private String successRedirectBase;
    @Value("${failure.redirect}")
    private String failureRedirectBase;
    @Value("${phase.id.step.one}")
    private String stepSessionInit;
    @Value("${phase.id.step.two}")
    private String stepSessionBuild;

    /* These strings define the error redirect URL query parameter that can be
     * used to guide the error page, the value matches the property langId that
     * fetches the correct language variant for the error message
     */
    @Value("${failure.param.internal}")
    private String errorParamInternal;
    @Value("${failure.param.phaseid}")
    private String errorParamPhaseId;
    @Value("${failure.param.invalid}")
    private String errorParamMessageInvalid;

    /* Do not reorder, because this affects mobile authentication.
     * These values are fetched from shibboleth2.xml and these must be
     * placed in the same order as the enum in Identifier.Types.
     */
    private String[] identifierTypes = { "AJP_hetu", "AJP_satu", "AJP_tfiKid", "AJP_eppn" };

    @Autowired
    public ServiceProvider(ProxyClient proxyClient) {
        this.proxyClient = proxyClient;
        this.historyService = PhaseIdHistoryService.getInstance();
    }

    public String processSAMLResponse(MultivaluedMap<String, String> headers, String tid, String pid,
            String logTag) {

        logger.debug("Processing SAML response with tid {} and pid {}", tid, pid);
        String newPhaseID = null;
        String redirectUrl = null;
        PhaseIdService phaseIdInitSession = null;

        try {
            phaseIdInitSession = new PhaseIdService(sharedSecret, timeInterval, hmacAlgorithm);
        } catch (Exception e) {
            logger.error("Error initializing new phaseIdService", e);
        }

        /* Token and phase IDs must be checked if they've been used already
         * in order to prevent replay attacks, there's only a small history
         * that needs to be checked so that performance isn't penalized
         */
        if (!historyService.areIdsConsumed(tid, pid)) {
            try {
                /* Both token ID and phase ID values must always match to a given set of rules
                 * since these values are exposed to public, they could have been tampered
                 */
                if (phaseIdInitSession.validateTidAndPid(tid, pid)
                        && phaseIdInitSession.verifyPhaseId(pid, tid, stepSessionInit)) {
                    newPhaseID = phaseIdInitSession.newPhaseId(tid, stepSessionBuild);
                }
            } catch (Exception e) {
                logger.error("<<{}>> Error verifying or generating next phase ID", logTag, e);
            }
        } else {
            logger.warn("Received already consumed token and phase IDs!!");
        }

        logger.debug("Got inbound SAML2 message, request token ID: {}, request phase ID: {}", tid, pid);
        logger.debug("--> next phase ID: {}", newPhaseID);

        if (newPhaseID != null) {
            Map<String, String> sessionData = new HashMap<>();

            /* There must be an identifier in the SAML message, since only trusted IdPs are accepted
             * and they always supply a proper identifier value that can be mapped to a specific value
             */
            if (checkAndParseIdentifierAndData(headers, sessionData)) {

                ProxyMessageDTO message = null;
                try {
                    message = proxyClient.updateSession(sessionData, tid, newPhaseID, logTag);
                } catch (InternalErrorException e) {
                    logger.error("<<{}>> Internal error occurred connection to proxy", logTag, e);
                    redirectUrl = createErrorURL(logTag, errorParamInternal);
                } catch (InvalidRequestException e) {
                    logger.error("<<{}>> Invalid request occurred connection to proxy", logTag, e);
                    redirectUrl = createErrorURL(logTag, errorParamMessageInvalid);
                } catch (IOException e) {
                    logger.error("<<{}>> Error in connection to proxy", logTag, e);
                    redirectUrl = createErrorURL(logTag, errorParamInternal);
                }

                String pidFromProxy = message.getPhaseId();
                String tidFromProxy = message.getTokenId();

                String idpCallUrl = successRedirectBase + tidFromProxy + "&pid=" + pidFromProxy + "&tag=" + logTag;
                logger.debug("Redirect URL to IdP: " + idpCallUrl);

                redirectUrl = idpCallUrl;

            } else {
                logger.warn("<<{}>> No identifier found from SAML message", logTag);

                redirectUrl = createErrorURL(logTag, errorParamMessageInvalid);
            }
        } else {
            logger.warn("<<{}>> Got invalid phase ID", logTag);

            redirectUrl = createErrorURL(logTag, errorParamPhaseId);
        }
        return redirectUrl;
    }

    /** The identifier type must be explicitly informed to Proxy since it can't know it without
     * excessive checks, note that this relies on the Shibboleth SP's configuration which must be
     * identical to what the shared API is using. This method returns true once first matching
     * identifier type has been found
     */
    private boolean checkAndParseIdentifierAndData(final MultivaluedMap<String, String> headers,
            Map<String, String> sessionData) {
        for (int i = 0; i < identifierTypes.length; i++) {
            if (StringUtils.isNotBlank(headers.getFirst(identifierTypes[i]))) {
                Set<String> headerNames = headers.keySet();

                logger.debug("--" + Identifier.typeKey + " <--> " + Identifier.Types.values()[i]);

                sessionData.put(Identifier.typeKey, "" + Identifier.Types.values()[i]);

                headerNames.forEach(headerName -> {
                    String headerValue = headers.getFirst(headerName);
                    /* All session data what the actual IdP has sent
                     * must be delivered to Proxy since the SP doesn't
                     * do anything with the data, it just delivers them
                     */
                    if (StringUtils.isNotBlank(headerValue)) {
                        sessionData.put(headerName, headerValue);
                    }
                });
                return true;
            }
        }

        return false;
    }

    private String createErrorURL(String logTag, String message) {
        return failureRedirectBase + "?t=" + logTag + "&m=" + message;
    }
}