fi.vm.kapa.identification.adapter.service.SessionParserService.java Source code

Java tutorial

Introduction

Here is the source code for fi.vm.kapa.identification.adapter.service.SessionParserService.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.adapter.service;

import fi.vm.kapa.identification.adapter.utils.AuthenticationHandlerUtils;
import fi.vm.kapa.identification.service.TupasStampIdService;

import org.apache.commons.codec.digest.DigestUtils;
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.Service;
import org.springframework.util.CollectionUtils;

import javax.annotation.PostConstruct;
import javax.ws.rs.core.MultivaluedMap;

import java.io.UnsupportedEncodingException;
import java.time.Clock;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

@Service
public class SessionParserService {

    private static final Logger logger = LoggerFactory.getLogger(SessionParserService.class);
    public static final int STAMP_VALIDITY_SECONDS = 300;
    public static final String CREATED_TS = "created";
    private static final long ATTRIBUTE_EXPIRY_TIME_MS = 5000;
    private final Clock clock = Clock.systemUTC();

    private Map<String, String> tupasProperties = new HashMap<>();
    private String tupasFormTemplate;
    private Map<String, String> declRefs = new HashMap<>();
    private TupasStampIdService tupasStampIdService;

    private String redirectUrlBase;
    private AdapterPropertyMapper propertyMapper;
    private AuthenticationHandlerUtils authenticationHandlerUtils;

    private static final String HETU_REGEXP = "^[0-9]{6}[+-Aa][0-9]{3}[A-Za-z0-9]$";
    private static final Pattern HETU_PATTERN = Pattern.compile(HETU_REGEXP);

    @Autowired
    public SessionParserService(@Value("${redirect.url.base}") String redirectUrlBase,
            AdapterPropertyMapper propertyMapper, AuthenticationHandlerUtils authenticationHandlerUtils) {
        this.redirectUrlBase = redirectUrlBase;
        this.propertyMapper = propertyMapper;
        this.authenticationHandlerUtils = authenticationHandlerUtils;
        this.tupasStampIdService = TupasStampIdService.getInstance();
    }

    @PostConstruct
    public void initSessionParser() {
        tupasProperties = propertyMapper.getTupasProperties();
        tupasFormTemplate = propertyMapper.getTupasFormTemplate();
        Set<Map.Entry<String, String>> tupasEntries = tupasProperties.entrySet().stream()
                .filter(entry -> entry.getKey().startsWith("BANK_URL_TFI_")).collect(Collectors.toSet());
        tupasEntries
                .forEach(entry -> declRefs.put(entry.getValue(), entry.getKey().replaceFirst("BANK_URL_TFI_", "")));
    }

    public String createInitResult(MultivaluedMap<String, String> requestParams) {
        String template;
        String declRef = requestParams.getFirst("declRef");
        String selectionId = declRefs.get(declRef);
        String token = authenticationHandlerUtils.createToken();
        String timeStampStr = String.valueOf(this.clock.millis());
        String tupasCounterStr = tupasStampIdService.getTupasStampCounter(timeStampStr.length());
        // replace any existing
        requestParams.putSingle("ADPT_TSTAMP", String.valueOf(timeStampStr) + tupasCounterStr);

        authenticationHandlerUtils.insertIntoCache(token, requestParams);

        logger.debug("Creating result string for IdP selection '{}'", selectionId);

        String actionUrl = tupasProperties.get("BANK_URL_" + selectionId);
        String version = tupasProperties.get("BANK_VERSION_" + selectionId);
        String rcvId = tupasProperties.get("BANK_RCVID_" + selectionId);
        String lang = requestParams.getFirst("lang").toUpperCase();
        String timestamp = requestParams.getFirst("ADPT_TSTAMP");
        String idType = tupasProperties.get("TUPAS_IDTYPE");
        String returnLink = propertyMapper.getTupasReturnLinkBase() + token;
        String cancelLink = propertyMapper.getTupasCancelLink() + token;
        String rejectLink = propertyMapper.getTupasRejectLink();
        String keyVersion = tupasProperties.get("BANK_KEYVERS_" + selectionId);
        String tupasMac = createTupasRequestMac(version, rcvId, lang, timestamp, idType, returnLink, cancelLink,
                rejectLink, keyVersion, tupasProperties.get("BANK_SECRET_" + selectionId));

        logger.info("Start Tupas identification for token={} ADPT_TSTAMP={}", token,
                requestParams.getFirst("ADPT_TSTAMP"));

        template = tupasFormTemplate.replace("TUPAS_ACTION_URL", actionUrl).replace("TUPAS_VERSION", version)
                .replace("TUPAS_RCVID", rcvId).replace("ADPT_LANG", lang).replace("ADPT_TSTAMP", timestamp)
                .replace("TUPAS_IDTYPE", idType).replace("ADPT_RETURN", returnLink)
                .replace("ADPT_CANCEL", cancelLink).replace("ADPT_REJECT", rejectLink)
                .replace("TUPAS_KEYVERS", keyVersion).replace("TUPAS_MAC", tupasMac);

        return template;
    }

    public String parseSessionData(final MultivaluedMap<String, String> sessionData) {
        String redirectUrl = null;
        String tokenId = sessionData.getFirst("token");
        MultivaluedMap<String, String> requestParams = authenticationHandlerUtils.popFromCache(tokenId);
        if (!hasHetu(sessionData)) {
            String custId = sessionData.getFirst("B02K_CUSTID");
            logger.warn("Tupas identification did not return HETU, aborting. token={}, B02K_CUSTID={}", tokenId,
                    custId);
            return null;
        }
        if (!CollectionUtils.isEmpty(requestParams)) {
            if (!isIdentifierPreserved(sessionData, requestParams)) {
                logger.warn("Tampered response received " + sessionData);
                return null;
            }
            if (!isIdentifierValid(sessionData, STAMP_VALIDITY_SECONDS)) {
                logger.warn("Expired response received " + sessionData);
                return null;
            }
            String declRef = requestParams.getFirst("declRef");
            String selectionId = declRefs.get(declRef);
            String calculatedMac = createTupasVerificationMac(sessionData.getFirst("B02K_VERS"),
                    sessionData.getFirst("B02K_TIMESTMP"), sessionData.getFirst("B02K_IDNBR"),
                    sessionData.getFirst("B02K_STAMP"), sessionData.getFirst("B02K_CUSTNAME"),
                    sessionData.getFirst("B02K_KEYVERS"), sessionData.getFirst("B02K_ALG"),
                    sessionData.getFirst("B02K_CUSTID"), sessionData.getFirst("B02K_CUSTTYPE"),
                    tupasProperties.get("BANK_SECRET_" + selectionId));

            if (calculatedMac.equals(sessionData.getFirst("B02K_MAC"))) {
                Map<String, String> attributes = new HashMap<>();
                attributes.put(propertyMapper.getSessionAttributeMap().get("B02K_CUSTID"),
                        sessionData.getFirst("B02K_CUSTID"));
                attributes.put(propertyMapper.getSessionAttributeMap().get("B02K_CUSTNAME"),
                        sessionData.getFirst("B02K_CUSTNAME"));
                attributes.put(CREATED_TS, Long.toString(clock.millis()));
                authenticationHandlerUtils.addToAttributesCache(tokenId, attributes);
                logger.info("Accepting Tupas identification for token={} B02K_STAMP={}", tokenId,
                        sessionData.getFirst("B02K_STAMP"));
                redirectUrl = redirectUrlBase + "?token=" + tokenId + "&ckey=" + requestParams.getFirst("ckey");
            } else {
                logger.warn("Tupas MAC calculation failed, values don't match");
            }
        }

        logger.debug("Generated redirect URL: {}", redirectUrl);
        return redirectUrl;
    }

    private boolean isIdentifierValid(MultivaluedMap<String, String> sessionData, long validitySeconds) {
        String stamp = sessionData.getFirst("B02K_STAMP");
        if (null != stamp && stamp.length() == 20) {
            try {
                long creationTimeSec = Long.parseLong(stamp.substring(0, 13)) / 1000;
                long nowSec = this.clock.millis() / 1000;
                if (nowSec < (creationTimeSec + validitySeconds)) {
                    return true;
                }
            } catch (NumberFormatException e) {
                logger.warn("Unexpected B02K_STAMP in session data " + sessionData);
                return false;
            }
        }
        return false;
    }

    private boolean isIdentifierPreserved(MultivaluedMap<String, String> sessionData,
            MultivaluedMap<String, String> requestParams) {
        return null != sessionData.getFirst("B02K_STAMP")
                && sessionData.getFirst("B02K_STAMP").equals(requestParams.getFirst("ADPT_TSTAMP"));
    }

    boolean hasHetu(MultivaluedMap<String, String> sessionData) {
        if (null != sessionData) {
            if ("01".equals(sessionData.getFirst("B02K_CUSTTYPE"))
                    && looksLikeHetu(sessionData.getFirst("B02K_CUSTID"))) {
                return true;
            } else if ("08".equals(sessionData.getFirst("B02K_CUSTTYPE"))
                    && looksLikeHetu(sessionData.getFirst("B02K_CUSTID"))) {
                logger.warn("B02K_CUSTTYPE=08: test user in SPankki and landsbanken");
                return true;
            } else if ("08".equals(sessionData.getFirst("B02K_CUSTTYPE"))) {
                logger.warn("B02K_CUSTTYPE=08: business user not supported");
                return false;
            }
        }
        return false;
    }

    boolean looksLikeHetu(String value) {
        if (null == value) {
            return false;
        } else {
            return HETU_PATTERN.matcher(value).matches();
        }
    }

    public Map<String, String> getSessionAttributes(final MultivaluedMap<String, String> attrsParams) {
        String token = attrsParams.getFirst("token");
        Map<String, String> attributes = authenticationHandlerUtils.popFromAttributesCache(token);
        if (null == attributes) {
            logger.info("No attributes for token={}", token);
            return null;
        } else {
            if (areAttributesValid(attributes)) {
                Map<String, String> values = new HashMap<>();
                for (Map.Entry<String, String> entry : attributes.entrySet()) {
                    if (!entry.getKey().equals(CREATED_TS)) {
                        values.put(entry.getKey(), entry.getValue());
                    }
                }
                logger.info("Attributes retrieved, token={}, attributes={}", token, values);
                return values;
            } else {
                logger.info("Discarding expired attributes for token={}, attributes={}request={}", token,
                        attributes, attrsParams);
                return null;
            }
        }
    }

    private boolean areAttributesValid(Map<String, String> attributes) {
        if (null != attributes && null != attributes.get(CREATED_TS)) {
            Long created = Long.parseLong(attributes.get(CREATED_TS));
            long now = clock.millis();
            if (now - created <= ATTRIBUTE_EXPIRY_TIME_MS) {
                return true;
            } else {
                logger.info("Expired attributes found, time={} created={}, timeout={}", now, created,
                        ATTRIBUTE_EXPIRY_TIME_MS);
                return false;
            }
        }
        return false;
    }

    public String purgeSession(final MultivaluedMap<String, String> attrsParams) {
        MultivaluedMap<String, String> retval = authenticationHandlerUtils
                .popFromCache(attrsParams.getFirst("token"));
        if (null != retval) {
            String redirurl = propertyMapper.getIdp_authn_cancelLink() + "&ckey=" + retval.getFirst("ckey")
                    + "&token=" + attrsParams.getFirst("token");
            logger.debug("purge session redir url: " + redirurl);
            return redirurl;
        }
        return null;
    }

    String createTupasRequestMac(String version, String rcvId, String lang, String timestamp, String idType,
            String returnLink, String cancelLink, String rejectLink, String keyVersion, String sharedSecret) {
        String tupasString = "701&" + version + "&" + rcvId + "&" + lang + "&" + timestamp + "&" + idType + "&"
                + returnLink + "&" + cancelLink + "&" + rejectLink + "&" + keyVersion + "&03&" + sharedSecret + "&";

        logger.debug("Generated Tupas request MAC string:\n{}", tupasString);

        try {
            return DigestUtils.sha256Hex(tupasString.getBytes("ISO-8859-1")).toUpperCase();
        } catch (UnsupportedEncodingException e) {
            logger.error("Unable to calculate tupas request MAC", e);
            return null;
        }
    }

    String createTupasVerificationMac(String version, String timestamp, String idNbr, String origStamp,
            String custName, String keyVers, String algorithm, String custId, String custType,
            String sharedSecret) {
        String tupasString = version + "&" + timestamp + "&" + idNbr + "&" + origStamp + "&" + custName + "&"
                + keyVers + "&" + algorithm + "&" + custId + "&" + custType + "&" + sharedSecret + "&";

        logger.debug("Generated Tupas response MAC string:\n{}", tupasString);

        try {
            return DigestUtils.sha256Hex(tupasString.getBytes("ISO-8859-1")).toUpperCase();
        } catch (UnsupportedEncodingException e) {
            logger.error("Unable to calculate tupas response verification MAC", e);
            return null;
        }
    }
}