Java tutorial
/** * 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; } }