/******************************************************************************* * Copyright 2017 Graz University of Technology * EAAF-Core Components has been developed in a cooperation between EGIZ, * A-SIT+, A-SIT, and Graz University of Technology. * * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by * the European Commission - subsequent versions of the EUPL (the "Licence"); * You may not use this work except in compliance with the Licence. * You may obtain a copy of the Licence at: * https://joinup.ec.europa.eu/news/understanding-eupl-v12 * * Unless required by applicable law or agreed to in writing, software * distributed under the Licence is distributed on an "AS IS" basis, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the Licence for the specific language governing permissions and * limitations under the Licence. * * This product combines work with different licenses. See the "NOTICE" text * file for details on the various modules and licenses. * The "NOTICE" text file is part of the distribution. Any derivative works * that you distribute must include a readable copy of the "NOTICE" text file. *******************************************************************************/ /******************************************************************************* *******************************************************************************/ /******************************************************************************* *******************************************************************************/ package at.gv.egiz.eaaf.modules.pvp2.idp.impl.builder; import java.security.MessageDigest; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import org.apache.commons.lang3.StringUtils; import org.joda.time.DateTime; import org.opensaml.common.xml.SAMLConstants; import org.opensaml.saml2.core.Assertion; import org.opensaml.saml2.core.Attribute; import org.opensaml.saml2.core.AttributeQuery; import org.opensaml.saml2.core.AttributeStatement; import org.opensaml.saml2.core.Audience; import org.opensaml.saml2.core.AudienceRestriction; import org.opensaml.saml2.core.AuthnContext; import org.opensaml.saml2.core.AuthnContextClassRef; import org.opensaml.saml2.core.AuthnRequest; import org.opensaml.saml2.core.AuthnStatement; import org.opensaml.saml2.core.Conditions; import org.opensaml.saml2.core.Issuer; import org.opensaml.saml2.core.NameID; import org.opensaml.saml2.core.RequestedAuthnContext; import org.opensaml.saml2.core.Subject; import org.opensaml.saml2.core.SubjectConfirmation; import org.opensaml.saml2.core.SubjectConfirmationData; import org.opensaml.saml2.core.impl.AuthnRequestImpl; import org.opensaml.saml2.metadata.AssertionConsumerService; import org.opensaml.saml2.metadata.AttributeConsumingService; import org.opensaml.saml2.metadata.EntityDescriptor; import org.opensaml.saml2.metadata.NameIDFormat; import org.opensaml.saml2.metadata.RequestedAttribute; import org.opensaml.saml2.metadata.SPSSODescriptor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.util.Base64Utils; import at.gv.egiz.eaaf.core.api.data.EAAFConstants; import at.gv.egiz.eaaf.core.api.data.ILoALevelMapper; import at.gv.egiz.eaaf.core.api.idp.IAuthData; import at.gv.egiz.eaaf.core.api.idp.ISPConfiguration; import at.gv.egiz.eaaf.core.api.idp.slo.SLOInformationInterface; import at.gv.egiz.eaaf.core.exceptions.UnavailableAttributeException; import at.gv.egiz.eaaf.core.impl.data.Pair; import at.gv.egiz.eaaf.core.impl.idp.controller.protocols.RequestImpl; import at.gv.egiz.eaaf.core.impl.utils.Random; import at.gv.egiz.eaaf.modules.pvp2.PVPConstants; import at.gv.egiz.eaaf.modules.pvp2.exception.PVP2Exception; import at.gv.egiz.eaaf.modules.pvp2.exception.QAANotSupportedException; import at.gv.egiz.eaaf.modules.pvp2.idp.api.builder.ISubjectNameIdGenerator; import at.gv.egiz.eaaf.modules.pvp2.idp.exception.ResponderErrorException; import at.gv.egiz.eaaf.modules.pvp2.idp.exception.UnprovideableAttributeException; import at.gv.egiz.eaaf.modules.pvp2.idp.impl.PVPSProfilePendingRequest; import at.gv.egiz.eaaf.modules.pvp2.impl.builder.PVPAttributeBuilder; import at.gv.egiz.eaaf.modules.pvp2.impl.utils.QAALevelVerifier; import at.gv.egiz.eaaf.modules.pvp2.impl.utils.SAML2Utils; @Service("PVP2AssertionBuilder") public class PVP2AssertionBuilder implements PVPConstants { private static final Logger log = LoggerFactory.getLogger(PVP2AssertionBuilder.class); @Autowired private ILoALevelMapper loaLevelMapper; @Autowired private ISubjectNameIdGenerator subjectNameIdGenerator; /** * Build a PVP assertion as response for a SAML2 AttributeQuery request * * @param issuerEntityID EnitiyID, which should be used for this IDP response * @param attrQuery AttributeQuery request from Service-Provider * @param attrList List of PVP response attributes * @param now Current time * @param validTo ValidTo time of the assertion * @param qaaLevel QAA level of the authentication * @param sessionIndex SAML2 SessionIndex, which should be included * * @return PVP 2.1 Assertion * @throws PVP2Exception */ public Assertion buildAssertion(String issuerEntityID, AttributeQuery attrQuery, List attrList, DateTime now, DateTime validTo, String qaaLevel, String sessionIndex) throws PVP2Exception { AuthnContextClassRef authnContextClassRef = SAML2Utils.createSAMLObject(AuthnContextClassRef.class); authnContextClassRef.setAuthnContextClassRef(qaaLevel); NameID subjectNameID = SAML2Utils.createSAMLObject(NameID.class); subjectNameID.setFormat(attrQuery.getSubject().getNameID().getFormat()); subjectNameID.setValue(attrQuery.getSubject().getNameID().getValue()); SubjectConfirmationData subjectConfirmationData = null; return buildGenericAssertion(issuerEntityID, attrQuery.getIssuer().getValue(), now, authnContextClassRef, attrList, subjectNameID, subjectConfirmationData, sessionIndex, validTo); } /** * Build a PVP 2.1 assertion as response of a SAML2 AuthnRequest * * @param issuerEntityID EnitiyID, which should be used for this IDP response * @param pendingReq Current processed pendingRequest DAO * @param authnRequest Current processed PVP AuthnRequest * @param authData AuthenticationData of the user, which is already authenticated * @param peerEntity SAML2 EntityDescriptor of the service-provider, which receives the response * @param date TimeStamp * @param assertionConsumerService SAML2 endpoint of the service-provider, which should be used * @param sloInformation Single LogOut information DAO * @return * @throws PVP2Exception */ public Assertion buildAssertion(String issuerEntityID, PVPSProfilePendingRequest pendingReq, AuthnRequest authnRequest, IAuthData authData, EntityDescriptor peerEntity, DateTime date, AssertionConsumerService assertionConsumerService, SLOInformationInterface sloInformation) throws PVP2Exception { ISPConfiguration oaParam = pendingReq.getServiceProviderConfiguration(); AuthnContextClassRef authnContextClassRef = SAML2Utils.createSAMLObject(AuthnContextClassRef.class); //check if authn. request contains LoA RequestedAuthnContext reqAuthnContext = authnRequest.getRequestedAuthnContext(); if (reqAuthnContext == null) { authnContextClassRef.setAuthnContextClassRef(authData.getEIDASQAALevel()); } else { //authn. request requests LoA levels. To LoA validation List reqAuthnContextClassRefIt = reqAuthnContext.getAuthnContextClassRefs(); //get matching mode from authn. request String loaMatchingMode = EAAFConstants.EIDAS_LOA_MATCHING_MINIMUM; if (StringUtils.isNotEmpty(reqAuthnContext.getComparison().toString())) loaMatchingMode = reqAuthnContext.getComparison().toString(); //get requested LoAs if (reqAuthnContextClassRefIt.size() == 0) { QAALevelVerifier.verifyQAALevel(authData.getEIDASQAALevel(), oaParam.getRequiredLoA(), loaMatchingMode); authnContextClassRef.setAuthnContextClassRef(authData.getEIDASQAALevel()); } else { List eIDASLoaFromRequest = new ArrayList(); for (AuthnContextClassRef authnClassRef : reqAuthnContextClassRefIt) { String qaa_uri = authnClassRef.getAuthnContextClassRef(); if (!qaa_uri.trim().startsWith(EAAFConstants.EIDAS_LOA_PREFIX)) { if (loaLevelMapper != null) { log.debug("Find no eIDAS LoA. Start mapping process ... " ); eIDASLoaFromRequest.add(loaLevelMapper.mapToeIDASLoA(qaa_uri.trim())); } else log.debug("AuthnRequest contains no eIDAS LoA. NO LoA mapper FOUND, ignore " + "'" + qaa_uri.trim() + "'"); } else eIDASLoaFromRequest.add(qaa_uri.trim()); } //stop process if no supported LoA scheme is requested if (eIDASLoaFromRequest.isEmpty()) { log.info("Authn. request contains no supported LoA level. Stop authentication process ... "); throw new QAANotSupportedException("No supported LoA in Authn. request"); } //verifiy LoAs from request to authentication LoA QAALevelVerifier.verifyQAALevel(authData.getEIDASQAALevel(), eIDASLoaFromRequest , loaMatchingMode); authnContextClassRef.setAuthnContextClassRef(authData.getEIDASQAALevel()); } } //load SPSS decriptor from service-provider metadata SPSSODescriptor spSSODescriptor = peerEntity.getSPSSODescriptor(SAMLConstants.SAML20P_NS); //add Attributes to Assertion List attrList = new ArrayList(); if (spSSODescriptor.getAttributeConsumingServices() != null && spSSODescriptor.getAttributeConsumingServices().size() > 0) { Integer aIdx = authnRequest.getAttributeConsumingServiceIndex(); int idx = 0; AttributeConsumingService attributeConsumingService = null; if (aIdx != null) { idx = aIdx.intValue(); attributeConsumingService = spSSODescriptor .getAttributeConsumingServices().get(idx); } else { List attrConsumingServiceList = spSSODescriptor.getAttributeConsumingServices(); for (AttributeConsumingService el : attrConsumingServiceList) { if (el.isDefault()) attributeConsumingService = el; } } /* * TODO: maybe use first AttributeConsumingService if no is selected * in request or on service is marked as default * */ if (attributeConsumingService == null ) { List attrConsumingServiceList = spSSODescriptor.getAttributeConsumingServices(); if (attrConsumingServiceList != null && !attrConsumingServiceList.isEmpty()) attributeConsumingService = attrConsumingServiceList.get(0); } if (attributeConsumingService != null) { Iterator it = attributeConsumingService .getRequestAttributes().iterator(); while (it.hasNext()) { RequestedAttribute reqAttribut = it.next(); try { Attribute attr = PVPAttributeBuilder.buildAttribute( reqAttribut.getName(), oaParam, authData); if (attr == null) { if (reqAttribut.isRequired()) { throw new UnprovideableAttributeException( reqAttribut.getName()); } } else { attrList.add(attr); } } catch (UnavailableAttributeException e) { log.info( "Attribute generation for " + reqAttribut.getFriendlyName() + " not possible."); if (reqAttribut.isRequired()) { throw new UnprovideableAttributeException( reqAttribut.getName()); } } catch (PVP2Exception e) { log.info( "Attribute generation failed! for " + reqAttribut.getFriendlyName()); if (reqAttribut.isRequired()) { throw new UnprovideableAttributeException( reqAttribut.getName()); } } catch (Exception e) { log.warn( "General Attribute generation failed! for " + reqAttribut.getFriendlyName(), e); if (reqAttribut.isRequired()) { throw new UnprovideableAttributeException( reqAttribut.getName()); } } } } } //generate subjectNameId NameID subjectNameID = SAML2Utils.createSAMLObject(NameID.class); Pair subjectNameIdPair = subjectNameIdGenerator.generateSubjectNameId(authData, oaParam); subjectNameID.setValue(subjectNameIdPair.getFirst()); subjectNameID.setNameQualifier(subjectNameIdPair.getSecond()); //get NameIDFormat from request String nameIDFormat = NameID.TRANSIENT; AuthnRequest authnReq = (AuthnRequestImpl) authnRequest; if (authnReq.getNameIDPolicy() != null && StringUtils.isNotEmpty(authnReq.getNameIDPolicy().getFormat())) { nameIDFormat = authnReq.getNameIDPolicy().getFormat(); } else { //get NameIDFormat from metadata List metadataNameIDFormats = spSSODescriptor.getNameIDFormats(); if (metadataNameIDFormats != null) { for (NameIDFormat el : metadataNameIDFormats) { if (NameID.PERSISTENT.equals(el.getFormat())) { nameIDFormat = NameID.PERSISTENT; break; } else if (NameID.TRANSIENT.equals(el.getFormat()) || NameID.UNSPECIFIED.equals(el.getFormat())) break; } } } if (NameID.TRANSIENT.equals(nameIDFormat) || NameID.UNSPECIFIED.equals(nameIDFormat)) { String random = Random.nextHexRandom32(); String nameID = subjectNameID.getValue(); try { MessageDigest md = MessageDigest.getInstance("SHA-1"); byte[] hash = md.digest((nameID + random).getBytes("ISO-8859-1")); subjectNameID.setValue(Base64Utils.encodeToString(hash)); subjectNameID.setNameQualifier(null); subjectNameID.setFormat(NameID.TRANSIENT); } catch (Exception e) { log.warn("PVP2 subjectNameID error", e); throw new ResponderErrorException("pvp2.13", null, e); } } else subjectNameID.setFormat(nameIDFormat); String sessionIndex = null; //if request is a reauthentication and NameIDFormat match reuse old session information if (StringUtils.isNotEmpty(authData.getNameID()) && StringUtils.isNotEmpty(authData.getNameIDFormat()) && nameIDFormat.equals(authData.getNameIDFormat())) { subjectNameID.setValue(authData.getNameID()); sessionIndex = authData.getSessionIndex(); } // if (StringUtils.isEmpty(sessionIndex)) sessionIndex = SAML2Utils.getSecureIdentifier(); SubjectConfirmationData subjectConfirmationData = SAML2Utils .createSAMLObject(SubjectConfirmationData.class); subjectConfirmationData.setInResponseTo(authnRequest.getID()); subjectConfirmationData.setNotOnOrAfter(new DateTime(authData.getSsoSessionValidTo().getTime())); // subjectConfirmationData.setNotBefore(date); //set 'recipient' attribute in subjectConformationData subjectConfirmationData.setRecipient(assertionConsumerService.getLocation()); //set IP address of the user machine as 'Address' attribute in subjectConformationData String usersIPAddress = pendingReq.getGenericData( RequestImpl.DATAID_REQUESTER_IP_ADDRESS, String.class); if (StringUtils.isNotEmpty(usersIPAddress)) subjectConfirmationData.setAddress(usersIPAddress); //set SLO information sloInformation.setUserNameIdentifier(subjectNameID.getValue()); sloInformation.setNameIDFormat(subjectNameID.getFormat()); sloInformation.setSessionIndex(sessionIndex); return buildGenericAssertion(issuerEntityID, peerEntity.getEntityID(), date, authnContextClassRef, attrList, subjectNameID, subjectConfirmationData, sessionIndex, subjectConfirmationData.getNotOnOrAfter()); } /** * * @param issuer IDP EntityID * @param entityID Service Provider EntityID * @param date * @param authnContextClassRef * @param attrList * @param subjectNameID * @param subjectConfirmationData * @param sessionIndex * @param isValidTo * @return * @throws ConfigurationException */ public Assertion buildGenericAssertion(String issuer, String entityID, DateTime date, AuthnContextClassRef authnContextClassRef, List attrList, NameID subjectNameID, SubjectConfirmationData subjectConfirmationData, String sessionIndex, DateTime isValidTo) throws ResponderErrorException { Assertion assertion = SAML2Utils.createSAMLObject(Assertion.class); AuthnContext authnContext = SAML2Utils .createSAMLObject(AuthnContext.class); authnContext.setAuthnContextClassRef(authnContextClassRef); AuthnStatement authnStatement = SAML2Utils .createSAMLObject(AuthnStatement.class); authnStatement.setAuthnInstant(date); authnStatement.setSessionIndex(sessionIndex); authnStatement.setAuthnContext(authnContext); assertion.getAuthnStatements().add(authnStatement); AttributeStatement attributeStatement = SAML2Utils .createSAMLObject(AttributeStatement.class); attributeStatement.getAttributes().addAll(attrList); if (attributeStatement.getAttributes().size() > 0) { assertion.getAttributeStatements().add(attributeStatement); } Subject subject = SAML2Utils.createSAMLObject(Subject.class); subject.setNameID(subjectNameID); SubjectConfirmation subjectConfirmation = SAML2Utils .createSAMLObject(SubjectConfirmation.class); subjectConfirmation.setMethod(SubjectConfirmation.METHOD_BEARER); subjectConfirmation.setSubjectConfirmationData(subjectConfirmationData); subject.getSubjectConfirmations().add(subjectConfirmation); Conditions conditions = SAML2Utils.createSAMLObject(Conditions.class); AudienceRestriction audienceRestriction = SAML2Utils .createSAMLObject(AudienceRestriction.class); Audience audience = SAML2Utils.createSAMLObject(Audience.class); audience.setAudienceURI(entityID); audienceRestriction.getAudiences().add(audience); conditions.setNotBefore(date); conditions.setNotOnOrAfter(isValidTo); conditions.getAudienceRestrictions().add(audienceRestriction); assertion.setConditions(conditions); Issuer issuerObj = SAML2Utils.createSAMLObject(Issuer.class); if (issuer.endsWith("/")) issuer = issuer.substring(0, issuer.length()-1); issuerObj.setValue(issuer); issuerObj.setFormat(NameID.ENTITY); assertion.setIssuer(issuerObj); assertion.setSubject(subject); assertion.setID(SAML2Utils.getSecureIdentifier()); assertion.setIssueInstant(date); return assertion; } }