Java tutorial
// Copyright 2009 Google Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package com.google.enterprise.adaptor.secmgr.servlets; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Predicate; import com.google.common.base.Strings; import com.google.common.collect.Iterables; import com.google.enterprise.adaptor.secmgr.authncontroller.ExportedState; import com.google.enterprise.adaptor.secmgr.common.SecurityManagerUtil; import com.google.enterprise.adaptor.secmgr.modules.SamlClient; import com.google.enterprise.adaptor.secmgr.saml.OpenSamlUtil; import com.google.enterprise.adaptor.secmgr.saml.SamlLogUtil; import org.joda.time.DateTime; import org.joda.time.DateTimeComparator; import org.joda.time.DateTimeUtils; import org.opensaml.saml2.core.Assertion; import org.opensaml.saml2.core.Attribute; import org.opensaml.saml2.core.AttributeStatement; import org.opensaml.saml2.core.Audience; import org.opensaml.saml2.core.AudienceRestriction; import org.opensaml.saml2.core.Conditions; import org.opensaml.saml2.core.Issuer; import org.opensaml.saml2.core.NameIDType; import org.opensaml.saml2.core.Response; import org.opensaml.saml2.core.Subject; import org.opensaml.saml2.core.SubjectConfirmation; import org.opensaml.saml2.core.SubjectConfirmationData; import org.opensaml.xml.XMLObject; import java.io.IOException; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; import javax.annotation.concurrent.Immutable; /** * A parser to disassemble and validate a SAML Response element. */ @Immutable public final class ResponseParser { private static final Logger LOGGER = Logger.getLogger(ResponseParser.class.getName()); private static final DateTimeComparator dtComparator = DateTimeComparator.getInstance(); private final SamlClient client; private final String recipient; private final Response response; private final String sessionId; private final long now; private final Assertion assertion; private ResponseParser(SamlClient client, String recipient, Response response, String sessionId) { this.client = client; this.recipient = recipient; this.response = response; this.sessionId = sessionId; this.now = DateTimeUtils.currentTimeMillis(); this.assertion = findSuitableAssertion(); } public static ResponseParser make(SamlClient client, String recipient, Response response, String sessionId) { return new ResponseParser(client, recipient, response, sessionId); } /** Log messages as info. */ private void inform(String... messages) { for (String message : messages) { LOGGER.info(SecurityManagerUtil.sessionLogMessage(sessionId, message)); } } /** Log messages as warnings. */ private void warn(String... messages) { for (String message : messages) { LOGGER.warning(SecurityManagerUtil.sessionLogMessage(sessionId, message)); } } /** * If condition is false then log messages as warnings. * <p> * This method is used to modify a chain of && boolean conditions. * For example: * <pre> * return name != null * && isValidName(name) * && hasSession(name); * </pre> * becomes: * <pre> * return warnIfFalse(name != null, "Name is null") * && warnIfFalse(isValidName(name), "Invalid name: " + name) * && warnIfFalse(hasSession(name), "Missing session for: " + name); * </pre> * <p> * @return value of condition */ private boolean warnIfFalse(boolean condition, String... messages) { if (!condition) { warn(messages); } return condition; } // to avoid having 3 try/catch loops in findSuitableAssertionHelper private Assertion findSuitableAssertion() { try { return findSuitableAssertionHelper(); } catch (IOException e) { LOGGER.log(Level.WARNING, "An error occurred but logger could not parse " + "the SamlResponse object.", e); return null; } } // TODO(cph): This logic is inadequate, but more or less the same as what the // GSA does. Instead of trying to find a single valid assertion, we should // analyze the assertions as a whole and combine their content. The SAML spec // allows the IdP to include arbitrary numbers of assertions, and with each // assertion an arbitrary number of statements. The spec explicitly states // that an assertion with multiple statements is completely equivalent to // multiple assertions, each with a single statement (provided the other parts // of the assertions match one another). It's the responsibility of the // relying party (us) to make sense of the information in whatever form the // IdP sends it. private Assertion findSuitableAssertionHelper() throws IOException { if (response.getAssertions().isEmpty()) { warn(SamlLogUtil.xmlMessage("Received no assertions in this response.", response)); } for (Assertion assertion : response.getAssertions()) { if (isAssertionValid(assertion)) { return assertion; } warn(SamlLogUtil.xmlMessage("Rejected assertion because it was invalid", assertion)); } warn(SamlLogUtil.xmlMessage("Could not find a valid assertion for this response", response)); return null; } /** * Is the response element valid? */ public boolean isResponseValid() { Issuer issuer = response.getIssuer(); return issuer == null || isValidIssuer(issuer); } /** * Get the response status code. * Must satisfy {@link #isResponseValid} prior to calling. */ public String getResponseStatus() { return response.getStatus().getStatusCode().getValue(); } /** * Are the assertions contained in this response valid? * Meaningful only when response status is "success". */ public boolean areAssertionsValid() { return assertion != null; } /** * Get the asserted subject. * Must satisfy {@link #areAssertionsValid} prior to calling. */ public String getSubject() { return assertion.getSubject().getNameID().getValue(); } /** * Get the expiration time for the subject verification. * Must satisfy {@link #areAssertionsValid} prior to calling. * * @return The expiration time, or null if there is none. */ public DateTime getExpirationTime() { DateTime time1 = assertion.getConditions().getNotOnOrAfter(); List<SubjectConfirmation> confirmations = assertion.getSubject().getSubjectConfirmations(); if (confirmations.isEmpty()) { return time1; } DateTime time2 = Iterables.find(confirmations, bearerPredicate).getSubjectConfirmationData() .getNotOnOrAfter(); return (time1 == null || dtComparator.compare(time1, time2) > 0) ? time2 : time1; } /** * Gets an exported-state object. This information is a security manager * extension. Must satisfy {@link #areAssertionsValid} prior to calling. */ public ExportedState getExportedState() { return getExportedState(assertion); } /** * Gets an exported-state object from a given assertion. This information is * a security manager extension. * * @param assertion The assertion to get the identities from. * @return A exported-state object, or {@code null} if there's none. */ @VisibleForTesting public static ExportedState getExportedState(Assertion assertion) { for (AttributeStatement attributeStatement : assertion.getAttributeStatements()) { for (Attribute attribute : attributeStatement.getAttributes()) { if (ExportedState.ATTRIBUTE_NAME.equals(attribute.getName())) { List<XMLObject> attributeValues = attribute.getAttributeValues(); if (attributeValues.size() == 1) { XMLObject attributeValue = attributeValues.get(0); String textContent = attributeValue.getDOM().getTextContent(); return ExportedState.fromJsonString(textContent); } } } } return null; } // **************** Validation primitives **************** private boolean isAssertionValid(Assertion assertion) { String validityDescription = "is empty (of statements): " + assertion.getAuthnStatements().isEmpty() + ", assertion issuer: " + assertion.getIssuer() + ", is valid issuer: " + isValidIssuer(assertion.getIssuer()) + ", is valid subject: " + isValidSubject(assertion.getSubject()) + ", are valid conditions: " + isValidConditions(assertion.getConditions()); inform(validityDescription); return !assertion.getAuthnStatements().isEmpty() && (assertion.getIssuer() != null) && isValidIssuer(assertion.getIssuer()) && isValidSubject(assertion.getSubject()) && isValidConditions(assertion.getConditions()); } private boolean isValidIssuer(Issuer issuer) { return warnIfFalse(issuer.getFormat() == null || NameIDType.ENTITY.equals(issuer.getFormat()), "Issuer contains a format: " + issuer.getFormat() + " but is not equal to expected format: " + NameIDType.ENTITY) && warnIfFalse(client.getPeerEntity().getEntityID().equals(issuer.getValue()), "Issuer value: " + issuer.getValue() + " is not equals to " + "expected value: " + client.getPeerEntity().getEntityID()); } private boolean isValidSubject(Subject subject) { return warnIfFalse(subject != null, "Subject is null.") && warnIfFalse(hasValidId(subject), "Subject has an invalid ID.") && warnIfFalse(areValidSubjectConfirmations(subject.getSubjectConfirmations()), "Subject does not have valid confirmations."); } // This is a security manager requirement; it's not mandated by the spec. private boolean hasValidId(Subject subject) { return warnIfFalse(subject.getBaseID() == null, "Subject BaseID is not null.") && warnIfFalse(subject.getNameID() != null, "Subject NameID is null") && warnIfFalse(!Strings.isNullOrEmpty(subject.getNameID().getValue()), "Subject NameID string is null or empty.") && warnIfFalse(subject.getEncryptedID() == null, "Subject contains an EncryptedID."); } private boolean areValidSubjectConfirmations(List<SubjectConfirmation> confirmations) { if (confirmations.isEmpty()) { // This violates the SAML spec, but the GSA has historically ignored this // information, so we must allow it. warn("SAML assertion received without subject confirmation"); return true; } Iterable<SubjectConfirmation> bearers = Iterables.filter(confirmations, bearerPredicate); return warnIfFalse(!Iterables.isEmpty(bearers), "SubjectConfirmations contains no bearers.") && warnIfFalse(Iterables.all(bearers, validBearerPredicate), "SubjectConfirmations were invalid."); } private Predicate<SubjectConfirmation> bearerPredicate = new Predicate<SubjectConfirmation>() { public boolean apply(SubjectConfirmation confirmation) { return OpenSamlUtil.BEARER_METHOD.equals(confirmation.getMethod()); } }; private Predicate<SubjectConfirmation> validBearerPredicate = new Predicate<SubjectConfirmation>() { public boolean apply(SubjectConfirmation bearer) { return isValidSubjectConfirmationData(bearer.getSubjectConfirmationData()); } }; private boolean isValidSubjectConfirmationData(SubjectConfirmationData data) { return warnIfFalse(recipient.equals(data.getRecipient()), "SubjectConfirmationData - recipient not equals : " + recipient + "but was instead: " + data.getRecipient()) && warnIfFalse(isValidExpiration(data.getNotOnOrAfter()), "Invalid expiration.") && warnIfFalse(client.getRequestId().equals((data.getInResponseTo())), "Assertion inResponseTo: " + data.getInResponseTo() + " was not equal to this client's RequestID: " + client.getRequestId()); } private boolean isValidExpiration(DateTime expiration) { return warnIfFalse(expiration != null, "Assertion expiration was null.") && warnIfFalse(SecurityManagerUtil.isRemoteOnOrAfterTimeValid(expiration.getMillis(), now), "The assertion's expiration time is invalid.", "Security Manager Current Time: " + new DateTime(now).toString(), "Assertion expiration:" + new DateTime(expiration.getMillis()).toString()); } // TODO(cph): This code needs to handle <OneTimeUse> and <ProxyRestriction> // conditions. private boolean isValidConditions(Conditions conditions) { return warnIfFalse(conditions != null, "Assertion conditions was null.") && warnIfFalse(isValidConditionNotBefore(conditions.getNotBefore()), "ConditionNotBefore is invalid: " + conditions.getNotBefore()) && warnIfFalse(isValidConditionNotOnOrAfter(conditions.getNotOnOrAfter()), "ConditionNotOnOrAfter is invalid: " + conditions.getNotOnOrAfter()) && warnIfFalse(isValidConditionAudienceRestrictions(conditions.getAudienceRestrictions()), "ConditionAudienceRestrictions is invalid: " + conditions.getAudienceRestrictions()); } private boolean isValidConditionNotBefore(DateTime notBefore) { return notBefore == null || SecurityManagerUtil.isRemoteBeforeTimeValid(notBefore.getMillis(), now); } private boolean isValidConditionNotOnOrAfter(DateTime notOnOrAfter) { return notOnOrAfter == null || SecurityManagerUtil.isRemoteOnOrAfterTimeValid(notOnOrAfter.getMillis(), now); } private boolean isValidConditionAudienceRestrictions(List<AudienceRestriction> restrictions) { String localEntityId = client.getLocalEntity().getEntityID(); for (AudienceRestriction restriction : restrictions) { for (Audience audience : restriction.getAudiences()) { if (localEntityId.equals(audience.getAudienceURI())) { return true; } } } return false; } }