You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cxf.apache.org by co...@apache.org on 2012/04/26 15:54:38 UTC
svn commit: r1330855 -
/cxf/trunk/rt/rs/security/xml/src/main/java/org/apache/cxf/rs/security/saml/sso/SAMLSSOResponseValidator.java
Author: coheigea
Date: Thu Apr 26 13:54:38 2012
New Revision: 1330855
URL: http://svn.apache.org/viewvc?rev=1330855&view=rev
Log:
Added a SAML 2.0 Protocol Response validator
Added:
cxf/trunk/rt/rs/security/xml/src/main/java/org/apache/cxf/rs/security/saml/sso/SAMLSSOResponseValidator.java
Added: cxf/trunk/rt/rs/security/xml/src/main/java/org/apache/cxf/rs/security/saml/sso/SAMLSSOResponseValidator.java
URL: http://svn.apache.org/viewvc/cxf/trunk/rt/rs/security/xml/src/main/java/org/apache/cxf/rs/security/saml/sso/SAMLSSOResponseValidator.java?rev=1330855&view=auto
==============================================================================
--- cxf/trunk/rt/rs/security/xml/src/main/java/org/apache/cxf/rs/security/saml/sso/SAMLSSOResponseValidator.java (added)
+++ cxf/trunk/rt/rs/security/xml/src/main/java/org/apache/cxf/rs/security/saml/sso/SAMLSSOResponseValidator.java Thu Apr 26 13:54:38 2012
@@ -0,0 +1,274 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 org.apache.cxf.rs.security.saml.sso;
+
+import java.util.List;
+import java.util.logging.Logger;
+
+import org.apache.cxf.common.logging.LogUtils;
+import org.apache.ws.security.WSSecurityException;
+import org.apache.ws.security.saml.ext.builder.SAML2Constants;
+import org.opensaml.saml2.core.AudienceRestriction;
+
+/**
+ * Validate a SAML 2.0 Protocol Response according to the Web SSO profile. The Response
+ * should be validated by the SAMLProtocolResponseValidator first.
+ *
+ * TODO If an <AuthnStatement> used to establish a security context for the principal contains a
+SessionNotOnOrAfter attribute, the security context SHOULD be discarded once this time is
+reached
+
+TODO The service provider MUST ensure that bearer assertions are not replayed, by maintaining the set of used
+ID values for the length of time for which the assertion would be considered valid based on the
+NotOnOrAfter attribute in the <SubjectConfirmationData>.
+ */
+public class SAMLSSOResponseValidator {
+
+ private static final Logger LOG = LogUtils.getL7dLogger(SAMLSSOResponseValidator.class);
+
+ private String issuerIDP;
+ private String assertionConsumerURL;
+ private String clientAddress;
+ private String requestId;
+ private String spIdentifier;
+
+ /**
+ * Validate a SAML 2 Protocol Response
+ * @param samlResponse
+ * @param postBinding
+ * @throws WSSecurityException
+ */
+ public void validateSamlResponse(
+ org.opensaml.saml2.core.Response samlResponse,
+ boolean postBinding
+ ) throws WSSecurityException {
+ // Check the Issuer
+ validateIssuer(samlResponse.getIssuer());
+
+ // The Response must contain at least one Assertion.
+ if (samlResponse.getAssertions() == null || samlResponse.getAssertions().isEmpty()) {
+ LOG.fine("The Response must contain at least one Assertion");
+ throw new WSSecurityException(WSSecurityException.FAILURE, "invalidSAMLsecurity");
+ }
+
+ // Validate Assertions
+ boolean foundValidSubject = false;
+ for (org.opensaml.saml2.core.Assertion assertion : samlResponse.getAssertions()) {
+ // Check the Issuer
+ if (assertion.getIssuer() == null) {
+ LOG.fine("Assertion Issuer must not be null");
+ throw new WSSecurityException(WSSecurityException.FAILURE, "invalidSAMLsecurity");
+ }
+ validateIssuer(assertion.getIssuer());
+
+ if (postBinding && assertion.getSignature() == null) {
+ LOG.fine("If the HTTP Post binding is used to deliver the Response, "
+ + "the enclosed assertions must be signed");
+ throw new WSSecurityException(WSSecurityException.FAILURE, "invalidSAMLsecurity");
+ }
+
+ // Check for AuthnStatements and validate the Subject accordingly
+ if (assertion.getAuthnStatements() != null
+ && !assertion.getAuthnStatements().isEmpty()) {
+ org.opensaml.saml2.core.Subject subject = assertion.getSubject();
+ if (validateAuthenticationSubject(subject)) {
+ validateAudienceRestrictionCondition(assertion.getConditions());
+ foundValidSubject = true;
+ }
+ }
+
+ }
+
+ if (!foundValidSubject) {
+ LOG.fine("The Response did not contain any Authentication Statement that matched "
+ + "the Subject Confirmation criteria");
+ throw new WSSecurityException(WSSecurityException.FAILURE, "invalidSAMLsecurity");
+ }
+ }
+
+ /**
+ * Validate the Issuer (if it exists)
+ */
+ private void validateIssuer(org.opensaml.saml2.core.Issuer issuer) throws WSSecurityException {
+ if (issuer == null) {
+ return;
+ }
+
+ // Issuer value must match Issuer IDP
+ if (!issuer.getValue().equals(issuerIDP)) {
+ LOG.fine("Issuer value: " + issuer.getValue() + " does not match issuer IDP: "
+ + issuerIDP);
+ throw new WSSecurityException(WSSecurityException.FAILURE, "invalidSAMLsecurity");
+ }
+
+ // Format must be nameid-format-entity
+ if (issuer.getFormat() != null
+ && !SAML2Constants.NAMEID_FORMAT_ENTITY.equals(issuer.getFormat())) {
+ LOG.fine("Issuer format is not null and does not equal: "
+ + SAML2Constants.NAMEID_FORMAT_ENTITY);
+ throw new WSSecurityException(WSSecurityException.FAILURE, "invalidSAMLsecurity");
+ }
+ }
+
+ /**
+ * Validate the Subject (of an Authentication Statement).
+ */
+ private boolean validateAuthenticationSubject(
+ org.opensaml.saml2.core.Subject subject
+ ) throws WSSecurityException {
+ if (subject.getSubjectConfirmations() == null) {
+ return false;
+ }
+ // We need to find a Bearer Subject Confirmation method
+ for (org.opensaml.saml2.core.SubjectConfirmation subjectConf
+ : subject.getSubjectConfirmations()) {
+ if (SAML2Constants.CONF_BEARER.equals(subjectConf.getMethod())) {
+ validateSubjectConfirmation(subjectConf.getSubjectConfirmationData());
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Validate a (Bearer) Subject Confirmation
+ */
+ private void validateSubjectConfirmation(
+ org.opensaml.saml2.core.SubjectConfirmationData subjectConfData
+ ) throws WSSecurityException {
+ if (subjectConfData == null) {
+ LOG.fine("Subject Confirmation Data of a Bearer Subject Confirmation is null");
+ throw new WSSecurityException(WSSecurityException.FAILURE, "invalidSAMLsecurity");
+ }
+
+ // Recipient must match assertion consumer URL
+ String recipient = subjectConfData.getRecipient();
+ if (recipient == null || !recipient.equals(assertionConsumerURL)) {
+ LOG.fine("Recipient " + recipient + " does not match assertion consumer URL "
+ + assertionConsumerURL);
+ throw new WSSecurityException(WSSecurityException.FAILURE, "invalidSAMLsecurity");
+ }
+
+ // TODO clock skew - future TTL required here.
+ // We must have a NotOnOrAfter timestamp
+ if (subjectConfData.getNotOnOrAfter() == null
+ || subjectConfData.getNotOnOrAfter().isAfterNow()) {
+ LOG.fine("Subject Conf Data does not contain NotOnOrAfter or it has expired");
+ throw new WSSecurityException(WSSecurityException.FAILURE, "invalidSAMLsecurity");
+ }
+
+ // Check address
+ if (subjectConfData.getAddress() != null
+ && !subjectConfData.getAddress().equals(clientAddress)) {
+ LOG.fine("Subject Conf Data address " + subjectConfData.getAddress() + " does match"
+ + " client address " + clientAddress);
+ throw new WSSecurityException(WSSecurityException.FAILURE, "invalidSAMLsecurity");
+ }
+
+ // It must not contain a NotBefore timestamp
+ if (subjectConfData.getNotBefore() != null) {
+ LOG.fine("The Subject Conf Data must not contain a NotBefore timestamp");
+ throw new WSSecurityException(WSSecurityException.FAILURE, "invalidSAMLsecurity");
+ }
+
+ // InResponseTo must match the AuthnRequest request Id
+ if (requestId != null && !requestId.equals(subjectConfData.getInResponseTo())) {
+ LOG.fine("The InResponseTo String does match the original request id " + requestId);
+ throw new WSSecurityException(WSSecurityException.FAILURE, "invalidSAMLsecurity");
+ }
+
+ }
+
+ private void validateAudienceRestrictionCondition(
+ org.opensaml.saml2.core.Conditions conditions
+ ) throws WSSecurityException {
+ if (conditions == null) {
+ LOG.fine("Conditions are null");
+ throw new WSSecurityException(WSSecurityException.FAILURE, "invalidSAMLsecurity");
+ }
+ List<AudienceRestriction> audienceRestrs = conditions.getAudienceRestrictions();
+ if (!matchSaml2AudienceRestriction(spIdentifier, audienceRestrs)) {
+ LOG.fine("Assertion does not contain unique subject provider identifier "
+ + spIdentifier + " in the audience restriction conditions");
+ throw new WSSecurityException(WSSecurityException.FAILURE, "invalidSAMLsecurity");
+ }
+ }
+
+
+ private boolean matchSaml2AudienceRestriction(
+ String appliesTo, List<AudienceRestriction> audienceRestrictions
+ ) {
+ boolean found = false;
+ if (audienceRestrictions != null && !audienceRestrictions.isEmpty()) {
+ for (AudienceRestriction audienceRestriction : audienceRestrictions) {
+ if (audienceRestriction.getAudiences() != null) {
+ for (org.opensaml.saml2.core.Audience audience : audienceRestriction.getAudiences()) {
+ if (appliesTo.equals(audience.getAudienceURI())) {
+ return true;
+ }
+ }
+ }
+ }
+ }
+
+ return found;
+ }
+
+ public String getIssuerIDP() {
+ return issuerIDP;
+ }
+
+ public void setIssuerIDP(String issuerIDP) {
+ this.issuerIDP = issuerIDP;
+ }
+
+ public String getAssertionConsumerURL() {
+ return assertionConsumerURL;
+ }
+
+ public void setAssertionConsumerURL(String assertionConsumerURL) {
+ this.assertionConsumerURL = assertionConsumerURL;
+ }
+
+ public String getClientAddress() {
+ return clientAddress;
+ }
+
+ public void setClientAddress(String clientAddress) {
+ this.clientAddress = clientAddress;
+ }
+
+ public String getRequestId() {
+ return requestId;
+ }
+
+ public void setRequestId(String requestId) {
+ this.requestId = requestId;
+ }
+
+ public String getSpIdentifier() {
+ return spIdentifier;
+ }
+
+ public void setSpIdentifier(String spIdentifier) {
+ this.spIdentifier = spIdentifier;
+ }
+
+
+}