/* * Copyright 2017 Graz University of Technology EAAF-Core Components has been developed in a * cooperation between EGIZ, A-SIT Plus, 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.time.Instant; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import javax.naming.ConfigurationException; import org.apache.commons.lang3.StringUtils; import org.opensaml.saml.common.xml.SAMLConstants; import org.opensaml.saml.saml2.core.Assertion; import org.opensaml.saml.saml2.core.Attribute; import org.opensaml.saml.saml2.core.AttributeQuery; import org.opensaml.saml.saml2.core.AttributeStatement; import org.opensaml.saml.saml2.core.Audience; import org.opensaml.saml.saml2.core.AudienceRestriction; import org.opensaml.saml.saml2.core.AuthnContext; import org.opensaml.saml.saml2.core.AuthnContextClassRef; import org.opensaml.saml.saml2.core.AuthnRequest; import org.opensaml.saml.saml2.core.AuthnStatement; import org.opensaml.saml.saml2.core.Conditions; import org.opensaml.saml.saml2.core.Issuer; import org.opensaml.saml.saml2.core.NameID; import org.opensaml.saml.saml2.core.NameIDType; import org.opensaml.saml.saml2.core.RequestedAuthnContext; import org.opensaml.saml.saml2.core.Subject; import org.opensaml.saml.saml2.core.SubjectConfirmation; import org.opensaml.saml.saml2.core.SubjectConfirmationData; import org.opensaml.saml.saml2.metadata.AssertionConsumerService; import org.opensaml.saml.saml2.metadata.AttributeConsumingService; import org.opensaml.saml.saml2.metadata.EntityDescriptor; import org.opensaml.saml.saml2.metadata.NameIDFormat; import org.opensaml.saml.saml2.metadata.RequestedAttribute; import org.opensaml.saml.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 In case of an error */ public Assertion buildAssertion(final String issuerEntityID, final AttributeQuery attrQuery, final List attrList, final Instant now, final Instant validTo, final String qaaLevel, final String sessionIndex) throws Pvp2Exception { final AuthnContextClassRef authnContextClassRef = Saml2Utils.createSamlObject(AuthnContextClassRef.class); authnContextClassRef.setURI(qaaLevel); final NameID subjectNameID = Saml2Utils.createSamlObject(NameID.class); subjectNameID.setFormat(attrQuery.getSubject().getNameID().getFormat()); subjectNameID.setValue(attrQuery.getSubject().getNameID().getValue()); final 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 PVP2 S-Profil Assertion * @throws Pvp2Exception In case of an error */ public Assertion buildAssertion(final String issuerEntityID, final PvpSProfilePendingRequest pendingReq, final AuthnRequest authnRequest, final IAuthData authData, final EntityDescriptor peerEntity, final Instant date, final AssertionConsumerService assertionConsumerService, final SloInformationInterface sloInformation) throws Pvp2Exception { final ISpConfiguration oaParam = pendingReq.getServiceProviderConfiguration(); final AuthnContextClassRef authnContextClassRef = Saml2Utils.createSamlObject(AuthnContextClassRef.class); // check if authn. request contains LoA final RequestedAuthnContext reqAuthnContext = authnRequest.getRequestedAuthnContext(); if (reqAuthnContext == null) { authnContextClassRef.setURI(authData.getEidasQaaLevel()); } else { // authn. request requests LoA levels. To LoA validation final List reqAuthnContextClassRefIt = reqAuthnContext.getAuthnContextClassRefs(); // get matching mode from authn. request String loaMatchingMode = EaafConstants.EIDAS_LOA_MATCHING_MINIMUM; if (reqAuthnContext.getComparison() != null && StringUtils.isNotEmpty(reqAuthnContext.getComparison().toString())) { loaMatchingMode = reqAuthnContext.getComparison().toString(); } // get requested LoAs if (reqAuthnContextClassRefIt.size() == 0) { QaaLevelVerifier.verifyQaaLevel(authData.getEidasQaaLevel(), oaParam.getRequiredLoA(), loaMatchingMode); authnContextClassRef.setURI(authData.getEidasQaaLevel()); } else { final List eidasLoaFromRequest = new ArrayList<>(); for (final AuthnContextClassRef authnClassRef : reqAuthnContextClassRefIt) { final String qaa_uri = authnClassRef.getURI(); if (!qaa_uri.trim().startsWith(EaafConstants.EIDAS_LOA_PREFIX)) { if (loaLevelMapper != null) { log.debug("Find no eIDAS LoA in AuthnReq. 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.setURI(authData.getEidasQaaLevel()); } } // load SPSS decriptor from service-provider metadata final SPSSODescriptor spSsoDescriptor = peerEntity.getSPSSODescriptor(SAMLConstants.SAML20P_NS); // add Attributes to Assertion final List attrList = new ArrayList<>(); if (spSsoDescriptor.getAttributeConsumingServices() != null && spSsoDescriptor.getAttributeConsumingServices().size() > 0) { final Integer aIdx = authnRequest.getAttributeConsumingServiceIndex(); int idx = 0; AttributeConsumingService attributeConsumingService = null; if (aIdx != null) { idx = aIdx; attributeConsumingService = spSsoDescriptor.getAttributeConsumingServices().get(idx); } else { final List attrConsumingServiceList = spSsoDescriptor.getAttributeConsumingServices(); for (final 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) { final List attrConsumingServiceList = spSsoDescriptor.getAttributeConsumingServices(); if (attrConsumingServiceList != null && !attrConsumingServiceList.isEmpty()) { attributeConsumingService = attrConsumingServiceList.get(0); } } if (attributeConsumingService != null) { final Iterator it = attributeConsumingService.getRequestedAttributes().iterator(); while (it.hasNext()) { final RequestedAttribute reqAttribut = it.next(); try { final Attribute attr = PvpAttributeBuilder.buildAttribute(reqAttribut.getName(), oaParam, authData); if (attr == null) { if (reqAttribut.isRequired()) { throw new UnprovideableAttributeException(reqAttribut.getName()); } } else { attrList.add(attr); } } catch (final UnavailableAttributeException e) { log.info( "Attribute generation for " + reqAttribut.getFriendlyName() + " not possible."); if (reqAttribut.isRequired()) { throw new UnprovideableAttributeException(reqAttribut.getName()); } } catch (final Pvp2Exception e) { log.info("Attribute generation failed! for " + reqAttribut.getFriendlyName()); if (reqAttribut.isRequired()) { throw new UnprovideableAttributeException(reqAttribut.getName()); } } catch (final Exception e) { log.warn("General Attribute generation failed! for " + reqAttribut.getFriendlyName(), e); if (reqAttribut.isRequired()) { throw new UnprovideableAttributeException(reqAttribut.getName()); } } } } } // generate subjectNameId final NameID subjectNameID = Saml2Utils.createSamlObject(NameID.class); final Pair subjectNameIdPair = subjectNameIdGenerator.generateSubjectNameId(authData, oaParam); subjectNameID.setValue(subjectNameIdPair.getFirst()); subjectNameID.setNameQualifier(subjectNameIdPair.getSecond()); // get NameIDFormat from request String nameIdFormat = NameIDType.TRANSIENT; final AuthnRequest authnReq = authnRequest; if (authnReq.getNameIDPolicy() != null && StringUtils.isNotEmpty(authnReq.getNameIDPolicy().getFormat())) { nameIdFormat = authnReq.getNameIDPolicy().getFormat(); } else { // get NameIDFormat from metadata final List metadataNameIdFormats = spSsoDescriptor.getNameIDFormats(); if (metadataNameIdFormats != null) { for (final NameIDFormat el : metadataNameIdFormats) { if (NameIDType.PERSISTENT.equals(el.getURI())) { nameIdFormat = NameIDType.PERSISTENT; break; } else if (NameIDType.TRANSIENT.equals(el.getURI()) || NameIDType.UNSPECIFIED.equals(el.getURI())) { break; } } } } if (NameIDType.TRANSIENT.equals(nameIdFormat) || NameIDType.UNSPECIFIED.equals(nameIdFormat)) { final String random = Random.nextHexRandom32(); final String nameID = subjectNameID.getValue(); try { final MessageDigest md = MessageDigest.getInstance("SHA-1"); final byte[] hash = md.digest((nameID + random).getBytes("ISO-8859-1")); subjectNameID.setValue(Base64Utils.encodeToString(hash)); subjectNameID.setNameQualifier(null); subjectNameID.setFormat(NameIDType.TRANSIENT); } catch (final Exception e) { log.warn("PVP2 subjectNameID error", e); throw new ResponderErrorException("internal.03", 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(); } final SubjectConfirmationData subjectConfirmationData = Saml2Utils.createSamlObject(SubjectConfirmationData.class); subjectConfirmationData.setInResponseTo(authnRequest.getID()); subjectConfirmationData.setNotOnOrAfter(authData.getSsoSessionValidTo()); // set 'recipient' attribute in subjectConformationData subjectConfirmationData.setRecipient(assertionConsumerService.getLocation()); // set IP address of the user machine as 'Address' attribute in // subjectConformationData final String usersIpAddress = pendingReq.getRawData(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()); } /** * Build generic part of PVP S-Profile Assertion. * * @param issuer IDP EntityID * @param entityID Service Provider EntityID * @param date Timestamp * @param authnContextClassRef SAML2 AuthnContextClassReference * @param attrList List of attributes * @param subjectNameID SubjectNameId * @param subjectConfirmationData SubjectConfirmationInformation * @param sessionIndex SessionIndex * @param isValidTo ValidTo Timestamp * @return PVP S-Profile Assertion * @throws ConfigurationException In case on an error */ public Assertion buildGenericAssertion(String issuer, final String entityID, final Instant date, final AuthnContextClassRef authnContextClassRef, final List attrList, final NameID subjectNameID, final SubjectConfirmationData subjectConfirmationData, final String sessionIndex, final Instant isValidTo) throws ResponderErrorException { final Assertion assertion = Saml2Utils.createSamlObject(Assertion.class); final AuthnContext authnContext = Saml2Utils.createSamlObject(AuthnContext.class); authnContext.setAuthnContextClassRef(authnContextClassRef); final AuthnStatement authnStatement = Saml2Utils.createSamlObject(AuthnStatement.class); authnStatement.setAuthnInstant(date); authnStatement.setSessionIndex(sessionIndex); authnStatement.setAuthnContext(authnContext); assertion.getAuthnStatements().add(authnStatement); final AttributeStatement attributeStatement = Saml2Utils.createSamlObject(AttributeStatement.class); attributeStatement.getAttributes().addAll(attrList); if (attributeStatement.getAttributes().size() > 0) { assertion.getAttributeStatements().add(attributeStatement); } final Subject subject = Saml2Utils.createSamlObject(Subject.class); subject.setNameID(subjectNameID); final SubjectConfirmation subjectConfirmation = Saml2Utils.createSamlObject(SubjectConfirmation.class); subjectConfirmation.setMethod(SubjectConfirmation.METHOD_BEARER); subjectConfirmation.setSubjectConfirmationData(subjectConfirmationData); subject.getSubjectConfirmations().add(subjectConfirmation); final Conditions conditions = Saml2Utils.createSamlObject(Conditions.class); final AudienceRestriction audienceRestriction = Saml2Utils.createSamlObject(AudienceRestriction.class); final Audience audience = Saml2Utils.createSamlObject(Audience.class); audience.setURI(entityID); audienceRestriction.getAudiences().add(audience); conditions.setNotBefore(date); conditions.setNotOnOrAfter(isValidTo); conditions.getAudienceRestrictions().add(audienceRestriction); assertion.setConditions(conditions); final Issuer issuerObj = Saml2Utils.createSamlObject(Issuer.class); if (issuer.endsWith("/")) { issuer = issuer.substring(0, issuer.length() - 1); } issuerObj.setValue(issuer); issuerObj.setFormat(NameIDType.ENTITY); assertion.setIssuer(issuerObj); assertion.setSubject(subject); assertion.setID(Saml2Utils.getSecureIdentifier()); assertion.setIssueInstant(date); return assertion; } }