diff --git a/cas-server-support-saml/src/main/java/org/jasig/cas/support/saml/SamlProtocolConstants.java b/cas-server-support-saml/src/main/java/org/jasig/cas/support/saml/SamlProtocolConstants.java new file mode 100644 index 000000000000..c6ff173c209e --- /dev/null +++ b/cas-server-support-saml/src/main/java/org/jasig/cas/support/saml/SamlProtocolConstants.java @@ -0,0 +1,41 @@ +/* + * Licensed to Apereo under one or more contributor license + * agreements. See the NOTICE file distributed with this work + * for additional information regarding copyright ownership. + * Apereo 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 the following location: + * + * 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.jasig.cas.support.saml; + +/** + * Class that exposes relevant constants and parameters to + * the SAML protocol. These include attribute names, pre-defined + * values and expected request parameter names as is specified + * by the protocol. + * + * @author Misagh Moayyed + * @since 4.1 + */ +public interface SamlProtocolConstants { + /** Constant representing the saml request. */ + String PARAMETER_SAML_REQUEST = "SAMLRequest"; + + /** Constant representing the saml response. */ + String PARAMETER_SAML_RESPONSE = "SAMLResponse"; + + /** Constant representing the saml relay state. */ + String PARAMETER_SAML_RELAY_STATE = "RelayState"; + +} diff --git a/cas-server-support-saml/src/main/java/org/jasig/cas/support/saml/authentication/principal/GoogleAccountsService.java b/cas-server-support-saml/src/main/java/org/jasig/cas/support/saml/authentication/principal/GoogleAccountsService.java index b5ad0c522af0..78c6eebdc7ad 100644 --- a/cas-server-support-saml/src/main/java/org/jasig/cas/support/saml/authentication/principal/GoogleAccountsService.java +++ b/cas-server-support-saml/src/main/java/org/jasig/cas/support/saml/authentication/principal/GoogleAccountsService.java @@ -18,32 +18,31 @@ */ package org.jasig.cas.support.saml.authentication.principal; -import org.apache.commons.codec.binary.Base64; -import org.apache.commons.io.IOUtils; + import org.jasig.cas.authentication.principal.AbstractWebApplicationService; import org.jasig.cas.authentication.principal.Response; import org.jasig.cas.services.RegisteredService; import org.jasig.cas.services.ServicesManager; -import org.jasig.cas.support.saml.util.SamlUtils; +import org.jasig.cas.support.saml.util.AbstractSaml20ObjectBuilder; import org.jasig.cas.util.ISOStandardDateFormat; import org.jdom.Document; import org.springframework.util.StringUtils; - +import org.jasig.cas.support.saml.SamlProtocolConstants; +import org.jasig.cas.support.saml.util.GoogleSaml20ObjectBuilder; +import org.joda.time.DateTime; +import org.opensaml.saml2.core.Assertion; +import org.opensaml.saml2.core.AuthnContext; +import org.opensaml.saml2.core.AuthnStatement; +import org.opensaml.saml2.core.Conditions; +import org.opensaml.saml2.core.NameID; +import org.opensaml.saml2.core.StatusCode; +import org.opensaml.saml2.core.Subject; import javax.servlet.http.HttpServletRequest; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.UnsupportedEncodingException; -import java.nio.charset.Charset; +import java.io.StringWriter; import java.security.PrivateKey; import java.security.PublicKey; -import java.security.SecureRandom; -import java.util.Calendar; -import java.util.Date; import java.util.HashMap; import java.util.Map; -import java.util.zip.DataFormatException; -import java.util.zip.Inflater; -import java.util.zip.InflaterInputStream; import org.jdom.Element; /** @@ -63,49 +62,7 @@ public class GoogleAccountsService extends AbstractWebApplicationService { private static final int HEX_HIGH_BITS_BITWISE_FLAG = 0x0f; private static final int HEX_RIGHT_SHIFT_COEFFICIENT = 4; - private static SecureRandom RANDOM_GENERATOR = new SecureRandom(); - - private static final char[] CHAR_MAPPINGS = { - 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p'}; - - private static final String CONST_PARAM_SERVICE = "SAMLRequest"; - - private static final String CONST_RELAY_STATE = "RelayState"; - - private static final String TEMPLATE_SAML_RESPONSE = - "\" IssueInstant=\"\" Version=\"2.0\"" - + " xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\"" - + " xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\"" - + " xmlns:xenc=\"http://www.w3.org/2001/04/xmlenc#\">" - + "" - + "" - + "" - + "\"" - + " IssueInstant=\"2003-04-17T00:46:02Z\" Version=\"2.0\"" - + " xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\">" - + "https://www.opensaml.org/IDP" - + "" - + "" - + "" - + "" - + "" - + "\" NotOnOrAfter=\"\" InResponseTo=\"\" />" - + "" - + "" - + "\">" - + "" - + "" - + "" - + "" - + "\">" - + "" - + "" - + "urn:oasis:names:tc:SAML:2.0:ac:classes:Password" - + "" - + "" - + "" - + ""; + private static final GoogleSaml20ObjectBuilder BUILDER = new GoogleSaml20ObjectBuilder(); private final String relayState; @@ -154,6 +111,8 @@ protected GoogleAccountsService(final String id, final String originalUrl, this.publicKey = publicKey; this.requestId = requestId; this.servicesManager = servicesManager; + + } /** @@ -168,15 +127,16 @@ protected GoogleAccountsService(final String id, final String originalUrl, public static GoogleAccountsService createServiceFrom( final HttpServletRequest request, final PrivateKey privateKey, final PublicKey publicKey, final ServicesManager servicesManager) { - final String relayState = request.getParameter(CONST_RELAY_STATE); + final String relayState = request.getParameter(SamlProtocolConstants.PARAMETER_SAML_RELAY_STATE); - final String xmlRequest = decodeAuthnRequestXML(request.getParameter(CONST_PARAM_SERVICE)); + final String xmlRequest = BUILDER.decodeSamlAuthnRequest( + request.getParameter(SamlProtocolConstants.PARAMETER_SAML_REQUEST)); if (!StringUtils.hasText(xmlRequest)) { return null; } - final Document document = SamlUtils.constructDocumentFromXmlString(xmlRequest); + final Document document = AbstractSaml20ObjectBuilder.constructDocumentFromXml(xmlRequest); if (document == null) { return null; @@ -194,10 +154,10 @@ public static GoogleAccountsService createServiceFrom( public Response getResponse(final String ticketId) { final Map parameters = new HashMap(); final String samlResponse = constructSamlResponse(); - final String signedResponse = SamlUtils.signSamlResponse(samlResponse, + final String signedResponse = BUILDER.signSamlResponse(samlResponse, this.privateKey, this.publicKey); - parameters.put("SAMLResponse", signedResponse); - parameters.put("RelayState", this.relayState); + parameters.put(SamlProtocolConstants.PARAMETER_SAML_RESPONSE, signedResponse); + parameters.put(SamlProtocolConstants.PARAMETER_SAML_RELAY_STATE, this.relayState); return Response.getPostResponse(getOriginalUrl(), parameters); } @@ -214,150 +174,42 @@ public boolean isLoggedOutAlready() { /** * Construct SAML response. - * + * See this reference for more info. * @return the SAML response */ private String constructSamlResponse() { - String samlResponse = TEMPLATE_SAML_RESPONSE; + final DateTime currentDateTime = DateTime.parse(new ISOStandardDateFormat().getCurrentDateAndTime()); + final DateTime notBeforeIssueInstant = DateTime.parse("2003-04-17T00:46:02Z"); - final Calendar c = Calendar.getInstance(); - c.setTime(new Date()); - c.add(Calendar.YEAR, 1); - final RegisteredService svc = this.servicesManager.findServiceBy(this); final String userId = svc.getUsernameAttributeProvider().resolveUsername(getPrincipal(), this); - final String currentDateTime = new ISOStandardDateFormat().getCurrentDateAndTime(); - samlResponse = samlResponse.replace("", userId); - samlResponse = samlResponse.replace("", createID()); - samlResponse = samlResponse.replace("", currentDateTime); - samlResponse = samlResponse.replace("", currentDateTime); - samlResponse = samlResponse.replaceAll("", currentDateTime); - samlResponse = samlResponse.replace("", createID()); - samlResponse = samlResponse.replaceAll("", getId()); - return samlResponse.replace("", this.requestId); - } + final org.opensaml.saml2.core.Response response = BUILDER.newResponse( + BUILDER.generateSecureRandomId(), + currentDateTime, + getId(), this); + response.setStatus(BUILDER.newStatus(StatusCode.SUCCESS_URI, null)); - /** - * Creates the SAML id. - * - * @return the id - */ - private static String createID() { - final byte[] bytes = new byte[SAML_RESPONSE_RANDOM_ID_LENGTH]; // 160 bits - RANDOM_GENERATOR.nextBytes(bytes); + final AuthnStatement authnStatement = BUILDER.newAuthnStatement( + AuthnContext.PASSWORD_AUTHN_CTX, currentDateTime); + final Assertion assertion = BUILDER.newAssertion(authnStatement, + "https://www.opensaml.org/IDP", + notBeforeIssueInstant, BUILDER.generateSecureRandomId()); - final char[] chars = new char[SAML_RESPONSE_ID_LENGTH]; + final Conditions conditions = BUILDER.newConditions(notBeforeIssueInstant, + currentDateTime, getId()); + assertion.setConditions(conditions); - for (int i = 0; i < bytes.length; i++) { - final int left = bytes[i] >> HEX_RIGHT_SHIFT_COEFFICIENT & HEX_HIGH_BITS_BITWISE_FLAG; - final int right = bytes[i] & HEX_HIGH_BITS_BITWISE_FLAG; - chars[i * 2] = CHAR_MAPPINGS[left]; - chars[i * 2 + 1] = CHAR_MAPPINGS[right]; - } + final Subject subject = BUILDER.newSubject(NameID.EMAIL, userId, + getId(), currentDateTime, this.requestId); + assertion.setSubject(subject); - return String.valueOf(chars); - } + response.getAssertions().add(assertion); - /** - * Decode authn request xml. - * - * @param encodedRequestXmlString the encoded request xml string - * @return the request - */ - private static String decodeAuthnRequestXML( - final String encodedRequestXmlString) { - if (encodedRequestXmlString == null) { - return null; - } - - final byte[] decodedBytes = base64Decode(encodedRequestXmlString); - - if (decodedBytes == null) { - return null; - } - - final String inflated = inflate(decodedBytes); - - if (inflated != null) { - return inflated; - } - - return zlibDeflate(decodedBytes); - } + final StringWriter writer = new StringWriter(); + BUILDER.marshalSamlXmlObject(response, writer); - /** - * Deflate the given bytes using zlib. - * - * @param bytes the bytes - * @return the converted string - */ - private static String zlibDeflate(final byte[] bytes) { - final ByteArrayInputStream bais = new ByteArrayInputStream(bytes); - final ByteArrayOutputStream baos = new ByteArrayOutputStream(); - final InflaterInputStream iis = new InflaterInputStream(bais); - final byte[] buf = new byte[DEFLATED_BYTE_ARRAY_BUFFER_LENGTH]; - - try { - int count = iis.read(buf); - while (count != -1) { - baos.write(buf, 0, count); - count = iis.read(buf); - } - return new String(baos.toByteArray(), Charset.defaultCharset()); - } catch (final Exception e) { - return null; - } finally { - IOUtils.closeQuietly(iis); - } - } - - /** - * Base64 decode. - * - * @param xml the xml - * @return the byte[] - */ - private static byte[] base64Decode(final String xml) { - try { - final byte[] xmlBytes = xml.getBytes("UTF-8"); - return Base64.decodeBase64(xmlBytes); - } catch (final Exception e) { - return null; - } - } - - /** - * Inflate the given byte array. - * - * @param bytes the bytes - * @return the string - */ - private static String inflate(final byte[] bytes) { - final Inflater inflater = new Inflater(true); - final byte[] xmlMessageBytes = new byte[INFLATED_BYTE_ARRAY_LENGTH]; - - final byte[] extendedBytes = new byte[bytes.length + 1]; - System.arraycopy(bytes, 0, extendedBytes, 0, bytes.length); - extendedBytes[bytes.length] = 0; - - inflater.setInput(extendedBytes); - - try { - final int resultLength = inflater.inflate(xmlMessageBytes); - inflater.end(); - - if (!inflater.finished()) { - throw new RuntimeException("buffer not large enough."); - } - - inflater.end(); - return new String(xmlMessageBytes, 0, resultLength, "UTF-8"); - } catch (final DataFormatException e) { - return null; - } catch (final UnsupportedEncodingException e) { - throw new RuntimeException("Cannot find encoding: UTF-8", e); - } + logger.debug("Generated Google SAML response: {}", writer.toString()); + return writer.toString(); } - } diff --git a/cas-server-support-saml/src/main/java/org/jasig/cas/support/saml/util/AbstractSaml20ObjectBuilder.java b/cas-server-support-saml/src/main/java/org/jasig/cas/support/saml/util/AbstractSaml20ObjectBuilder.java new file mode 100644 index 000000000000..65b66e2b8d59 --- /dev/null +++ b/cas-server-support-saml/src/main/java/org/jasig/cas/support/saml/util/AbstractSaml20ObjectBuilder.java @@ -0,0 +1,352 @@ +/* + * Licensed to Apereo under one or more contributor license + * agreements. See the NOTICE file distributed with this work + * for additional information regarding copyright ownership. + * Apereo 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 the following location: + * + * 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.jasig.cas.support.saml.util; + +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.io.IOUtils; +import org.jasig.cas.authentication.principal.WebApplicationService; +import org.jasig.cas.support.saml.authentication.principal.SamlService; +import org.joda.time.DateTime; +import org.opensaml.common.SAMLVersion; +import org.opensaml.saml2.core.Assertion; +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.AuthnStatement; +import org.opensaml.saml2.core.Conditions; +import org.opensaml.saml2.core.Issuer; +import org.opensaml.saml2.core.NameID; +import org.opensaml.saml2.core.Response; +import org.opensaml.saml2.core.Status; +import org.opensaml.saml2.core.StatusCode; +import org.opensaml.saml2.core.StatusMessage; +import org.opensaml.saml2.core.Subject; +import org.opensaml.saml2.core.SubjectConfirmation; +import org.opensaml.saml2.core.SubjectConfirmationData; +import org.springframework.util.StringUtils; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.UnsupportedEncodingException; +import java.security.SecureRandom; +import java.util.zip.DataFormatException; +import java.util.zip.Inflater; +import java.util.zip.InflaterInputStream; + +/** + * This is {@link AbstractSaml20ObjectBuilder}. + * to build saml2 objects. + * @author Misagh Moayyed mmoayyed@unicon.net + * @since 4.1 + */ +public abstract class AbstractSaml20ObjectBuilder extends AbstractSamlObjectBuilder { + private static final int HEX_HIGH_BITS_BITWISE_FLAG = 0x0f; + + /** + * Gets name id. + * + * @param nameIdFormat the name id format + * @param nameIdValue the name id value + * @return the name iD + */ + protected NameID getNameID(final String nameIdFormat, final String nameIdValue) { + final NameID nameId = newSamlObject(NameID.class); + nameId.setFormat(nameIdFormat); + nameId.setValue(nameIdValue); + return nameId; + } + + /** + * Create a new SAML response object. + * @param id the id + * @param issueInstant the issue instant + * @param recipient the recipient + * @param service the service + * @return the response + */ + public Response newResponse(final String id, final DateTime issueInstant, + final String recipient, final WebApplicationService service) { + + final Response samlResponse = newSamlObject(Response.class); + samlResponse.setID(id); + samlResponse.setIssueInstant(issueInstant); + samlResponse.setVersion(SAMLVersion.VERSION_20); + if (service instanceof SamlService) { + final SamlService samlService = (SamlService) service; + + if (samlService.getRequestID() != null) { + samlResponse.setInResponseTo(samlService.getRequestID()); + } + } + return samlResponse; + } + + /** + * Create a new SAML status object. + * + * @param codeValue the code value + * @param statusMessage the status message + * @return the status + */ + public Status newStatus(final String codeValue, final String statusMessage) { + final Status status = newSamlObject(Status.class); + final StatusCode code = newSamlObject(StatusCode.class); + code.setValue(codeValue); + status.setStatusCode(code); + if (StringUtils.hasText(statusMessage)) { + final StatusMessage message = newSamlObject(StatusMessage.class); + message.setMessage(statusMessage); + status.setStatusMessage(message); + } + return status; + } + + /** + * Create a new SAML1 response object. + * + * @param authnStatement the authn statement + * @param issuer the issuer + * @param issuedAt the issued at + * @param id the id + * @return the assertion + */ + public Assertion newAssertion(final AuthnStatement authnStatement, final String issuer, + final DateTime issuedAt, final String id) { + final Assertion assertion = newSamlObject(Assertion.class); + assertion.setID(id); + assertion.setIssueInstant(issuedAt); + assertion.setIssuer(newIssuer(issuer)); + assertion.getAuthnStatements().add(authnStatement); + return assertion; + } + + /** + * New issuer. + * + * @param issuerValue the issuer + * @return the issuer + */ + public Issuer newIssuer(final String issuerValue) { + final Issuer issuer = newSamlObject(Issuer.class); + issuer.setValue(issuerValue); + return issuer; + } + + /** + * New authn statement. + * + * @param contextClassRef the context class ref such as {@link AuthnContext#PASSWORD_AUTHN_CTX} + * @param authnInstant the authn instant + * @return the authn statement + */ + public AuthnStatement newAuthnStatement(final String contextClassRef, final DateTime authnInstant) { + final AuthnStatement stmt = newSamlObject(AuthnStatement.class); + final AuthnContext ctx = newSamlObject(AuthnContext.class); + + final AuthnContextClassRef classRef = newSamlObject(AuthnContextClassRef.class); + classRef.setAuthnContextClassRef(contextClassRef); + + ctx.setAuthnContextClassRef(classRef); + stmt.setAuthnContext(ctx); + stmt.setAuthnInstant(authnInstant); + + return stmt; + } + + /** + * New conditions element. + * + * @param notBefore the not before + * @param notOnOrAfter the not on or after + * @param audienceUri the service id + * @return the conditions + */ + public Conditions newConditions(final DateTime notBefore, final DateTime notOnOrAfter, final String audienceUri) { + final Conditions conditions = newSamlObject(Conditions.class); + conditions.setNotBefore(notBefore); + conditions.setNotOnOrAfter(notOnOrAfter); + + final AudienceRestriction audienceRestriction = newSamlObject(AudienceRestriction.class); + final Audience audience = newSamlObject(Audience.class); + audience.setAudienceURI(audienceUri); + audienceRestriction.getAudiences().add(audience); + conditions.getAudienceRestrictions().add(audienceRestriction); + return conditions; + } + + /** + * New subject element. + * + * @param nameIdFormat the name id format + * @param nameIdValue the name id value + * @param recipient the recipient + * @param notOnOrAfter the not on or after + * @param inResponseTo the in response to + * @return the subject + */ + public Subject newSubject(final String nameIdFormat, final String nameIdValue, + final String recipient, final DateTime notOnOrAfter, + final String inResponseTo) { + + final SubjectConfirmation confirmation = newSamlObject(SubjectConfirmation.class); + confirmation.setMethod(SubjectConfirmation.METHOD_BEARER); + + final SubjectConfirmationData data = newSamlObject(SubjectConfirmationData.class); + data.setRecipient(recipient); + data.setNotOnOrAfter(notOnOrAfter); + data.setInResponseTo(inResponseTo); + + confirmation.setSubjectConfirmationData(data); + + final Subject subject = newSamlObject(Subject.class); + subject.setNameID(getNameID(nameIdFormat, nameIdValue)); + subject.getSubjectConfirmations().add(confirmation); + return subject; + } + + @Override + public String generateSecureRandomId() { + final SecureRandom generator = new SecureRandom(); + final char[] charMappings = { + 'a', 'b', 'c', 'd', 'e', 'f', 'g', + 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', + 'p'}; + + final int charsLength = 40; + final int generatorBytesLength = 20; + final int shiftLength = 4; + + // 160 bits + final byte[] bytes = new byte[generatorBytesLength]; + generator.nextBytes(bytes); + + final char[] chars = new char[charsLength]; + for (int i = 0; i < bytes.length; i++) { + final int left = bytes[i] >> shiftLength & HEX_HIGH_BITS_BITWISE_FLAG; + final int right = bytes[i] & HEX_HIGH_BITS_BITWISE_FLAG; + chars[i * 2] = charMappings[left]; + chars[i * 2 + 1] = charMappings[right]; + } + return String.valueOf(chars); + } + + /** + * Deflate the given bytes using zlib. + * + * @param bytes the bytes + * @return the converted string + */ + private static String zlibDeflate(final byte[] bytes) { + final int bufferLength = 1024; + final ByteArrayInputStream bais = new ByteArrayInputStream(bytes); + final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + final InflaterInputStream iis = new InflaterInputStream(bais); + final byte[] buf = new byte[bufferLength]; + + try { + int count = iis.read(buf); + while (count != -1) { + baos.write(buf, 0, count); + count = iis.read(buf); + } + return new String(baos.toByteArray()); + } catch (final Exception e) { + return null; + } finally { + IOUtils.closeQuietly(iis); + } + } + + /** + * Base64 decode. + * + * @param xml the xml + * @return the byte[] + */ + private static byte[] base64Decode(final String xml) { + try { + final byte[] xmlBytes = xml.getBytes("UTF-8"); + return Base64.decodeBase64(xmlBytes); + } catch (final Exception e) { + return null; + } + } + + /** + * Inflate the given byte array. + * + * @param bytes the bytes + * @return the string + */ + private static String inflate(final byte[] bytes) { + final int bufferLength = 10000; + final Inflater inflater = new Inflater(true); + final byte[] xmlMessageBytes = new byte[bufferLength]; + + final byte[] extendedBytes = new byte[bytes.length + 1]; + System.arraycopy(bytes, 0, extendedBytes, 0, bytes.length); + extendedBytes[bytes.length] = 0; + + inflater.setInput(extendedBytes); + + try { + final int resultLength = inflater.inflate(xmlMessageBytes); + inflater.end(); + + if (!inflater.finished()) { + throw new RuntimeException("buffer not large enough."); + } + + inflater.end(); + return new String(xmlMessageBytes, 0, resultLength, "UTF-8"); + } catch (final DataFormatException e) { + return null; + } catch (final UnsupportedEncodingException e) { + throw new RuntimeException("Cannot find encoding: UTF-8", e); + } + } + + + + /** + * Decode authn request xml. + * + * @param encodedRequestXmlString the encoded request xml string + * @return the request + */ + public String decodeSamlAuthnRequest(final String encodedRequestXmlString) { + if (encodedRequestXmlString == null) { + return null; + } + + final byte[] decodedBytes = base64Decode(encodedRequestXmlString); + if (decodedBytes == null) { + return null; + } + + final String inflated = inflate(decodedBytes); + if (inflated != null) { + return inflated; + } + + return zlibDeflate(decodedBytes); + } +} diff --git a/cas-server-support-saml/src/main/java/org/jasig/cas/support/saml/util/SamlUtils.java b/cas-server-support-saml/src/main/java/org/jasig/cas/support/saml/util/AbstractSamlObjectBuilder.java similarity index 51% rename from cas-server-support-saml/src/main/java/org/jasig/cas/support/saml/util/SamlUtils.java rename to cas-server-support-saml/src/main/java/org/jasig/cas/support/saml/util/AbstractSamlObjectBuilder.java index 3c4910936dd9..af1740854af3 100644 --- a/cas-server-support-saml/src/main/java/org/jasig/cas/support/saml/util/SamlUtils.java +++ b/cas-server-support-saml/src/main/java/org/jasig/cas/support/saml/util/AbstractSamlObjectBuilder.java @@ -1,254 +1,396 @@ -/* - * Licensed to Apereo under one or more contributor license - * agreements. See the NOTICE file distributed with this work - * for additional information regarding copyright ownership. - * Apereo 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 the following location: - * - * 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.jasig.cas.support.saml.util; - -import org.jdom.Document; -import org.jdom.Element; -import org.jdom.input.DOMBuilder; -import org.jdom.input.SAXBuilder; -import org.jdom.output.XMLOutputter; -import org.w3c.dom.Node; - -import javax.xml.crypto.dsig.CanonicalizationMethod; -import javax.xml.crypto.dsig.DigestMethod; -import javax.xml.crypto.dsig.Reference; -import javax.xml.crypto.dsig.SignatureMethod; -import javax.xml.crypto.dsig.SignedInfo; -import javax.xml.crypto.dsig.Transform; -import javax.xml.crypto.dsig.XMLSignature; -import javax.xml.crypto.dsig.XMLSignatureFactory; -import javax.xml.crypto.dsig.dom.DOMSignContext; -import javax.xml.crypto.dsig.keyinfo.KeyInfo; -import javax.xml.crypto.dsig.keyinfo.KeyInfoFactory; -import javax.xml.crypto.dsig.keyinfo.KeyValue; -import javax.xml.crypto.dsig.spec.C14NMethodParameterSpec; -import javax.xml.crypto.dsig.spec.TransformParameterSpec; -import javax.xml.parsers.DocumentBuilderFactory; -import java.io.ByteArrayInputStream; -import java.io.StringWriter; -import java.nio.charset.Charset; -import java.security.PrivateKey; -import java.security.Provider; -import java.security.PublicKey; -import java.security.interfaces.DSAPublicKey; -import java.security.interfaces.RSAPublicKey; -import java.util.Collections; -import java.util.List; - -/** - * Utilities adopted from the Google sample code. - * - * @author Scott Battaglia - * @since 3.1 - */ -public final class SamlUtils { - - private static final String JSR_105_PROVIDER = "org.jcp.xml.dsig.internal.dom.XMLDSigRI"; - - private static final String SAML_PROTOCOL_NS_URI_V20 = "urn:oasis:names:tc:SAML:2.0:protocol"; - - /** - * The constructor is intentionally marked as private. - */ - private SamlUtils() { - // nothing to do - } - - /** - * Sign SAML response. - * - * @param samlResponse the SAML response - * @param privateKey the private key - * @param publicKey the public key - * @return the response - */ - public static String signSamlResponse(final String samlResponse, - final PrivateKey privateKey, final PublicKey publicKey) { - final Document doc = constructDocumentFromXmlString(samlResponse); - - if (doc != null) { - final Element signedElement = signSamlElement(doc.getRootElement(), - privateKey, publicKey); - doc.setRootElement((Element) signedElement.detach()); - return new XMLOutputter().outputString(doc); - } - throw new RuntimeException("Error signing SAML Response: Null document"); - } - - /** - * Construct document from xml string. - * - * @param xmlString the xml string - * @return the document - */ - public static Document constructDocumentFromXmlString(final String xmlString) { - try { - final SAXBuilder builder = new SAXBuilder(); - builder.setFeature("http://xml.org/sax/features/external-general-entities", false); - builder.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); - return builder - .build(new ByteArrayInputStream(xmlString.getBytes(Charset.defaultCharset()))); - } catch (final Exception e) { - return null; - } - } - - /** - * Sign SAML element. - * - * @param element the element - * @param privKey the priv key - * @param pubKey the pub key - * @return the element - */ - private static Element signSamlElement(final Element element, final PrivateKey privKey, - final PublicKey pubKey) { - try { - final String providerName = System.getProperty("jsr105Provider", - JSR_105_PROVIDER); - final XMLSignatureFactory sigFactory = XMLSignatureFactory - .getInstance("DOM", (Provider) Class.forName(providerName) - .newInstance()); - - final List envelopedTransform = Collections - .singletonList(sigFactory.newTransform(Transform.ENVELOPED, - (TransformParameterSpec) null)); - - final Reference ref = sigFactory.newReference("", sigFactory - .newDigestMethod(DigestMethod.SHA1, null), envelopedTransform, - null, null); - - // Create the SignatureMethod based on the type of key - SignatureMethod signatureMethod; - if (pubKey instanceof DSAPublicKey) { - signatureMethod = sigFactory.newSignatureMethod( - SignatureMethod.DSA_SHA1, null); - } else if (pubKey instanceof RSAPublicKey) { - signatureMethod = sigFactory.newSignatureMethod( - SignatureMethod.RSA_SHA1, null); - } else { - throw new RuntimeException( - "Error signing SAML element: Unsupported type of key"); - } - - final CanonicalizationMethod canonicalizationMethod = sigFactory - .newCanonicalizationMethod( - CanonicalizationMethod.INCLUSIVE_WITH_COMMENTS, - (C14NMethodParameterSpec) null); - - // Create the SignedInfo - final SignedInfo signedInfo = sigFactory.newSignedInfo( - canonicalizationMethod, signatureMethod, Collections - .singletonList(ref)); - - // Create a KeyValue containing the DSA or RSA PublicKey - final KeyInfoFactory keyInfoFactory = sigFactory - .getKeyInfoFactory(); - final KeyValue keyValuePair = keyInfoFactory.newKeyValue(pubKey); - - // Create a KeyInfo and add the KeyValue to it - final KeyInfo keyInfo = keyInfoFactory.newKeyInfo(Collections - .singletonList(keyValuePair)); - // Convert the JDOM document to w3c (Java XML signature API requires - // w3c - // representation) - final org.w3c.dom.Element w3cElement = toDom(element); - - // Create a DOMSignContext and specify the DSA/RSA PrivateKey and - // location of the resulting XMLSignature's parent element - final DOMSignContext dsc = new DOMSignContext(privKey, w3cElement); - - final org.w3c.dom.Node xmlSigInsertionPoint = getXmlSignatureInsertLocation(w3cElement); - dsc.setNextSibling(xmlSigInsertionPoint); - - // Marshal, generate (and sign) the enveloped signature - final XMLSignature signature = sigFactory.newXMLSignature(signedInfo, - keyInfo); - signature.sign(dsc); - - return toJdom(w3cElement); - - } catch (final Exception e) { - throw new RuntimeException("Error signing SAML element: " - + e.getMessage(), e); - } - } - - /** - * Gets the xml signature insert location. - * - * @param elem the elem - * @return the xml signature insert location - */ - private static Node getXmlSignatureInsertLocation(final org.w3c.dom.Element elem) { - org.w3c.dom.Node insertLocation = null; - org.w3c.dom.NodeList nodeList = elem.getElementsByTagNameNS( - SAML_PROTOCOL_NS_URI_V20, "Extensions"); - if (nodeList.getLength() != 0) { - insertLocation = nodeList.item(nodeList.getLength() - 1); - } else { - nodeList = elem.getElementsByTagNameNS(SAML_PROTOCOL_NS_URI_V20, - "Status"); - insertLocation = nodeList.item(nodeList.getLength() - 1); - } - return insertLocation; - } - - /** - * Convert the received jdom element to an Element. - * - * @param element the element - * @return the org.w3c.dom. element - */ - private static org.w3c.dom.Element toDom(final Element element) { - return toDom(element.getDocument()).getDocumentElement(); - } - - /** - * Convert the received jdom doc to a Document element. - * - * @param doc the doc - * @return the org.w3c.dom. document - */ - private static org.w3c.dom.Document toDom(final Document doc) { - try { - final XMLOutputter xmlOutputter = new XMLOutputter(); - final StringWriter elemStrWriter = new StringWriter(); - xmlOutputter.output(doc, elemStrWriter); - final byte[] xmlBytes = elemStrWriter.toString().getBytes(Charset.defaultCharset()); - final DocumentBuilderFactory dbf = DocumentBuilderFactory - .newInstance(); - dbf.setNamespaceAware(true); - return dbf.newDocumentBuilder().parse( - new ByteArrayInputStream(xmlBytes)); - } catch (final Exception e) { - return null; - } - } - - /** - * Convert to a jdom element. - * - * @param e the e - * @return the element - */ - private static Element toJdom(final org.w3c.dom.Element e) { - return new DOMBuilder().build(e); - } -} +/* + * Licensed to Apereo under one or more contributor license + * agreements. See the NOTICE file distributed with this work + * for additional information regarding copyright ownership. + * Apereo 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 the following location: + * + * 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.jasig.cas.support.saml.util; + +import org.jdom.Document; +import org.jdom.input.DOMBuilder; +import org.jdom.input.SAXBuilder; +import org.jdom.output.XMLOutputter; +import org.opensaml.Configuration; +import org.opensaml.DefaultBootstrap; +import org.opensaml.common.SAMLObject; +import org.opensaml.common.SAMLObjectBuilder; +import org.opensaml.common.impl.SecureRandomIdentifierGenerator; +import org.opensaml.common.xml.SAMLConstants; +import org.opensaml.xml.ConfigurationException; +import org.opensaml.xml.XMLObject; +import org.opensaml.xml.io.Marshaller; +import org.opensaml.xml.io.MarshallerFactory; +import org.opensaml.xml.schema.XSString; +import org.opensaml.xml.schema.impl.XSStringBuilder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Element; +import org.w3c.dom.Node; + +import javax.xml.crypto.dsig.CanonicalizationMethod; +import javax.xml.crypto.dsig.DigestMethod; +import javax.xml.crypto.dsig.Reference; +import javax.xml.crypto.dsig.SignatureMethod; +import javax.xml.crypto.dsig.SignedInfo; +import javax.xml.crypto.dsig.Transform; +import javax.xml.crypto.dsig.XMLSignature; +import javax.xml.crypto.dsig.XMLSignatureFactory; +import javax.xml.crypto.dsig.dom.DOMSignContext; +import javax.xml.crypto.dsig.keyinfo.KeyInfo; +import javax.xml.crypto.dsig.keyinfo.KeyInfoFactory; +import javax.xml.crypto.dsig.keyinfo.KeyValue; +import javax.xml.crypto.dsig.spec.C14NMethodParameterSpec; +import javax.xml.crypto.dsig.spec.TransformParameterSpec; +import javax.xml.namespace.QName; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.transform.OutputKeys; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; +import java.io.ByteArrayInputStream; +import java.io.StringWriter; +import java.lang.reflect.Field; +import java.nio.charset.Charset; +import java.security.PrivateKey; +import java.security.Provider; +import java.security.PublicKey; +import java.security.interfaces.DSAPublicKey; +import java.security.interfaces.RSAPublicKey; +import java.util.Collections; +import java.util.List; +/** + * An abstract builder to serve as the template handler + * for SAML1 and SAML2 responses. + * + * @author Misagh Moayyed mmoayyed@unicon.net + * @since 4.1 + */ +public abstract class AbstractSamlObjectBuilder { + /** + * The constant DEFAULT_ELEMENT_NAME_FIELD. + */ + protected static final String DEFAULT_ELEMENT_NAME_FIELD = "DEFAULT_ELEMENT_NAME"; + + /** + * The constant DEFAULT_ELEMENT_LOCAL_NAME_FIELD. + */ + protected static final String DEFAULT_ELEMENT_LOCAL_NAME_FIELD = "DEFAULT_ELEMENT_LOCAL_NAME"; + + /** Logger instance. **/ + protected final Logger logger = LoggerFactory.getLogger(this.getClass()); + + static { + try { + // Initialize OpenSAML default configuration + // (only needed once per classloader) + DefaultBootstrap.bootstrap(); + } catch (final ConfigurationException e) { + throw new IllegalStateException("Error initializing OpenSAML library.", e); + } + } + + /** + * Create a new SAML object. + * + * @param the generic type + * @param objectType the object type + * @return the t + */ + public final T newSamlObject(final Class objectType) { + final QName qName = getSamlObjectQName(objectType); + final SAMLObjectBuilder builder = (SAMLObjectBuilder) Configuration.getBuilderFactory().getBuilder(qName); + if (builder == null) { + throw new IllegalStateException("No SAMLObjectBuilder registered for class " + objectType.getName()); + } + return objectType.cast(builder.buildObject(qName)); + } + + /** + * Gets saml object QName. + * + * @param objectType the object type + * @return the saml object QName + * @throws RuntimeException the exception + */ + public QName getSamlObjectQName(final Class objectType) throws RuntimeException { + try { + final Field f = objectType.getField(DEFAULT_ELEMENT_NAME_FIELD); + final QName qName = (QName) f.get(null); + return qName; + } catch (final NoSuchFieldException e) { + throw new IllegalStateException("Cannot find field " + objectType.getName() + "." + DEFAULT_ELEMENT_NAME_FIELD); + } catch (final IllegalAccessException e) { + throw new IllegalStateException("Cannot access field " + objectType.getName() + "." + DEFAULT_ELEMENT_NAME_FIELD); + } + } + + /** + * Build the saml object based on its QName. + * + * @param objectType the object + * @param qName the QName + * @param the object type + * @return the saml object + */ + private T newSamlObject(final Class objectType, final QName qName) { + final SAMLObjectBuilder builder = (SAMLObjectBuilder) Configuration.getBuilderFactory().getBuilder(qName); + if (builder == null) { + throw new IllegalStateException("No SAMLObjectBuilder registered for class " + objectType.getName()); + } + return objectType.cast(builder.buildObject()); + } + + /** + * New attribute value. + * + * @param value the value + * @param elementName the element name + * @return the xS string + */ + protected final XSString newAttributeValue(final Object value, final QName elementName) { + final XSStringBuilder attrValueBuilder = new XSStringBuilder(); + final XSString stringValue = attrValueBuilder.buildObject(elementName, XSString.TYPE_NAME); + if (value instanceof String) { + stringValue.setValue((String) value); + } else { + stringValue.setValue(value.toString()); + } + return stringValue; + } + + /** + * Generate a secure random id. + * + * @return the secure id string + */ + public String generateSecureRandomId() { + try { + final SecureRandomIdentifierGenerator idGenerator = new SecureRandomIdentifierGenerator(); + return idGenerator.generateIdentifier(); + } catch (final Exception e) { + throw new IllegalStateException("Cannot create secure random ID generator for SAML message IDs.", e); + } + } + + /** + * Marshal the saml xml object to raw xml. + * + * @param object the object + * @param writer the writer + * @return the xml string + */ + public String marshalSamlXmlObject(final XMLObject object, final StringWriter writer) { + try { + final MarshallerFactory marshallerFactory = Configuration.getMarshallerFactory(); + final Marshaller marshaller = marshallerFactory.getMarshaller(object); + final Element element = marshaller.marshall(object); + element.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns", SAMLConstants.SAML20_NS); + element.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:xenc", "http://www.w3.org/2001/04/xmlenc#"); + + final TransformerFactory transFactory = TransformerFactory.newInstance(); + final Transformer transformer = transFactory.newTransformer(); + transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes"); + transformer.setOutputProperty(OutputKeys.INDENT, "yes"); + transformer.transform(new DOMSource(element), new StreamResult(writer)); + return writer.toString(); + } catch (final Exception e) { + throw new IllegalStateException("An error has occurred while marshalling SAML object to xml", e); + } + } + + /** + * Sign SAML response. + * + * @param samlResponse the SAML response + * @param privateKey the private key + * @param publicKey the public key + * @return the response + */ + public final String signSamlResponse(final String samlResponse, + final PrivateKey privateKey, final PublicKey publicKey) { + final Document doc = constructDocumentFromXml(samlResponse); + + if (doc != null) { + final org.jdom.Element signedElement = signSamlElement(doc.getRootElement(), + privateKey, publicKey); + doc.setRootElement((org.jdom.Element) signedElement.detach()); + return new XMLOutputter().outputString(doc); + } + throw new RuntimeException("Error signing SAML Response: Null document"); + } + + /** + * Construct document from xml string. + * + * @param xmlString the xml string + * @return the document + */ + public static Document constructDocumentFromXml(final String xmlString) { + try { + final SAXBuilder builder = new SAXBuilder(); + builder.setFeature("http://xml.org/sax/features/external-general-entities", false); + builder.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); + return builder + .build(new ByteArrayInputStream(xmlString.getBytes(Charset.defaultCharset()))); + } catch (final Exception e) { + return null; + } + } + + /** + * Sign SAML element. + * + * @param element the element + * @param privKey the priv key + * @param pubKey the pub key + * @return the element + */ + private static org.jdom.Element signSamlElement(final org.jdom.Element element, final PrivateKey privKey, + final PublicKey pubKey) { + try { + final String providerName = System.getProperty("jsr105Provider", + "org.jcp.xml.dsig.internal.dom.XMLDSigRI"); + + final XMLSignatureFactory sigFactory = XMLSignatureFactory + .getInstance("DOM", (Provider) Class.forName(providerName) + .newInstance()); + + final List envelopedTransform = Collections + .singletonList(sigFactory.newTransform(Transform.ENVELOPED, + (TransformParameterSpec) null)); + + final Reference ref = sigFactory.newReference("", sigFactory + .newDigestMethod(DigestMethod.SHA1, null), envelopedTransform, + null, null); + + // Create the SignatureMethod based on the type of key + SignatureMethod signatureMethod; + if (pubKey instanceof DSAPublicKey) { + signatureMethod = sigFactory.newSignatureMethod( + SignatureMethod.DSA_SHA1, null); + } else if (pubKey instanceof RSAPublicKey) { + signatureMethod = sigFactory.newSignatureMethod( + SignatureMethod.RSA_SHA1, null); + } else { + throw new RuntimeException("Error signing SAML element: Unsupported type of key"); + } + + final CanonicalizationMethod canonicalizationMethod = sigFactory + .newCanonicalizationMethod( + CanonicalizationMethod.INCLUSIVE_WITH_COMMENTS, + (C14NMethodParameterSpec) null); + + // Create the SignedInfo + final SignedInfo signedInfo = sigFactory.newSignedInfo( + canonicalizationMethod, signatureMethod, Collections + .singletonList(ref)); + + // Create a KeyValue containing the DSA or RSA PublicKey + final KeyInfoFactory keyInfoFactory = sigFactory + .getKeyInfoFactory(); + final KeyValue keyValuePair = keyInfoFactory.newKeyValue(pubKey); + + // Create a KeyInfo and add the KeyValue to it + final KeyInfo keyInfo = keyInfoFactory.newKeyInfo(Collections + .singletonList(keyValuePair)); + // Convert the JDOM document to w3c (Java XML signature API requires + // w3c + // representation) + final org.w3c.dom.Element w3cElement = toDom(element); + + // Create a DOMSignContext and specify the DSA/RSA PrivateKey and + // location of the resulting XMLSignature's parent element + final DOMSignContext dsc = new DOMSignContext(privKey, w3cElement); + + final org.w3c.dom.Node xmlSigInsertionPoint = getXmlSignatureInsertLocation(w3cElement); + dsc.setNextSibling(xmlSigInsertionPoint); + + // Marshal, generate (and sign) the enveloped signature + final XMLSignature signature = sigFactory.newXMLSignature(signedInfo, + keyInfo); + signature.sign(dsc); + + return toJdom(w3cElement); + + } catch (final Exception e) { + throw new RuntimeException("Error signing SAML element: " + + e.getMessage(), e); + } + } + + /** + * Gets the xml signature insert location. + * + * @param elem the elem + * @return the xml signature insert location + */ + private static Node getXmlSignatureInsertLocation(final org.w3c.dom.Element elem) { + org.w3c.dom.Node insertLocation = null; + org.w3c.dom.NodeList nodeList = elem.getElementsByTagNameNS( + SAMLConstants.SAML20P_NS, "Extensions"); + if (nodeList.getLength() != 0) { + insertLocation = nodeList.item(nodeList.getLength() - 1); + } else { + nodeList = elem.getElementsByTagNameNS(SAMLConstants.SAML20P_NS, "Status"); + insertLocation = nodeList.item(nodeList.getLength() - 1); + } + return insertLocation; + } + + /** + * Convert the received jdom element to an Element. + * + * @param element the element + * @return the org.w3c.dom. element + */ + private static org.w3c.dom.Element toDom(final org.jdom.Element element) { + return toDom(element.getDocument()).getDocumentElement(); + } + + /** + * Convert the received jdom doc to a Document element. + * + * @param doc the doc + * @return the org.w3c.dom. document + */ + private static org.w3c.dom.Document toDom(final Document doc) { + try { + final XMLOutputter xmlOutputter = new XMLOutputter(); + final StringWriter elemStrWriter = new StringWriter(); + xmlOutputter.output(doc, elemStrWriter); + final byte[] xmlBytes = elemStrWriter.toString().getBytes(Charset.defaultCharset()); + final DocumentBuilderFactory dbf = DocumentBuilderFactory + .newInstance(); + dbf.setNamespaceAware(true); + return dbf.newDocumentBuilder().parse( + new ByteArrayInputStream(xmlBytes)); + } catch (final Exception e) { + return null; + } + } + + /** + * Convert to a jdom element. + * + * @param e the e + * @return the element + */ + private static org.jdom.Element toJdom(final org.w3c.dom.Element e) { + return new DOMBuilder().build(e); + } +} + diff --git a/cas-server-support-saml/src/main/java/org/jasig/cas/support/saml/util/GoogleSaml20ObjectBuilder.java b/cas-server-support-saml/src/main/java/org/jasig/cas/support/saml/util/GoogleSaml20ObjectBuilder.java new file mode 100644 index 000000000000..4edcf650cee5 --- /dev/null +++ b/cas-server-support-saml/src/main/java/org/jasig/cas/support/saml/util/GoogleSaml20ObjectBuilder.java @@ -0,0 +1,54 @@ +/* + * Licensed to Apereo under one or more contributor license + * agreements. See the NOTICE file distributed with this work + * for additional information regarding copyright ownership. + * Apereo 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 the following location: + * + * 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.jasig.cas.support.saml.util; + +import org.opensaml.common.xml.SAMLConstants; +import org.opensaml.saml2.core.Response; +import org.opensaml.saml2.core.Status; +import org.opensaml.saml2.core.StatusCode; + +import javax.xml.XMLConstants; +import javax.xml.namespace.QName; +import java.lang.reflect.Field; + +/** + * This is {@link org.jasig.cas.support.saml.util.GoogleSaml20ObjectBuilder} that + * attempts to build the saml response. QName based on the spec described here: + * https://developers.google.com/google-apps/sso/saml_reference_implementation_web#samlReferenceImplementationWebSetupChangeDomain + * @author Misagh Moayyed mmoayyed@unicon.net + * @since 4.1.0 + */ +public class GoogleSaml20ObjectBuilder extends AbstractSaml20ObjectBuilder { + @Override + public final QName getSamlObjectQName(final Class objectType) throws RuntimeException { + try { + final Field f = objectType.getField(DEFAULT_ELEMENT_LOCAL_NAME_FIELD); + final String name = f.get(null).toString(); + + if (objectType.equals(Response.class) || objectType.equals(Status.class) + || objectType.equals(StatusCode.class)) { + return new QName(SAMLConstants.SAML20P_NS, name, "samlp"); + } + return new QName(SAMLConstants.SAML20_NS, name, XMLConstants.DEFAULT_NS_PREFIX); + } catch (final Exception e){ + throw new IllegalStateException("Cannot access field " + objectType.getName() + "." + DEFAULT_ELEMENT_LOCAL_NAME_FIELD); + } + } +} diff --git a/cas-server-support-saml/src/main/java/org/jasig/cas/support/saml/util/Saml10ObjectBuilder.java b/cas-server-support-saml/src/main/java/org/jasig/cas/support/saml/util/Saml10ObjectBuilder.java new file mode 100644 index 000000000000..3d7a796a9ea6 --- /dev/null +++ b/cas-server-support-saml/src/main/java/org/jasig/cas/support/saml/util/Saml10ObjectBuilder.java @@ -0,0 +1,262 @@ +/* + * Licensed to Apereo under one or more contributor license + * agreements. See the NOTICE file distributed with this work + * for additional information regarding copyright ownership. + * Apereo 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 the following location: + * + * 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.jasig.cas.support.saml.util; + +import org.jasig.cas.authentication.principal.WebApplicationService; +import org.jasig.cas.support.saml.authentication.SamlAuthenticationMetaDataPopulator; +import org.jasig.cas.support.saml.authentication.principal.SamlService; +import org.joda.time.DateTime; +import org.opensaml.common.SAMLVersion; +import org.opensaml.common.binding.BasicSAMLMessageContext; +import org.opensaml.saml1.binding.encoding.HTTPSOAP11Encoder; +import org.opensaml.saml1.core.Assertion; +import org.opensaml.saml1.core.Attribute; +import org.opensaml.saml1.core.AttributeStatement; +import org.opensaml.saml1.core.AttributeValue; +import org.opensaml.saml1.core.Audience; +import org.opensaml.saml1.core.AudienceRestrictionCondition; +import org.opensaml.saml1.core.AuthenticationStatement; +import org.opensaml.saml1.core.Conditions; +import org.opensaml.saml1.core.ConfirmationMethod; +import org.opensaml.saml1.core.NameIdentifier; +import org.opensaml.saml1.core.Response; +import org.opensaml.saml1.core.Status; +import org.opensaml.saml1.core.StatusCode; +import org.opensaml.saml1.core.StatusMessage; +import org.opensaml.saml1.core.Subject; +import org.opensaml.saml1.core.SubjectConfirmation; +import org.opensaml.ws.transport.http.HttpServletResponseAdapter; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.xml.namespace.QName; +import java.util.Collection; +import java.util.Date; +import java.util.Map; + +/** + * This is the response builder for Saml1 Protocol. + * + * @author Misagh Moayyed mmoayyed@unicon.net + * @since 4.1 + */ +public final class Saml10ObjectBuilder extends AbstractSamlObjectBuilder { + + private static final String CONFIRMATION_METHOD = "urn:oasis:names:tc:SAML:1.0:cm:artifact"; + + /** + * Encoder to wrap the saml response in a SOAP envelope. + */ + private final HTTPSOAP11Encoder encoder = new CasHTTPSOAP11Encoder(); + + /** + * Create a new SAML response object. + * @param id the id + * @param issueInstant the issue instant + * @param recipient the recipient + * @param service the service + * @return the response + */ + public Response newResponse(final String id, final DateTime issueInstant, + final String recipient, final WebApplicationService service) { + + final Response samlResponse = newSamlObject(Response.class); + samlResponse.setID(id); + samlResponse.setIssueInstant(issueInstant); + samlResponse.setVersion(SAMLVersion.VERSION_11); + samlResponse.setRecipient(recipient); + if (service instanceof SamlService) { + final SamlService samlService = (SamlService) service; + + if (samlService.getRequestID() != null) { + samlResponse.setInResponseTo(samlService.getRequestID()); + } + } + return samlResponse; + } + + /** + * Create a new SAML1 response object. + * + * @param authnStatement the authn statement + * @param issuer the issuer + * @param issuedAt the issued at + * @param id the id + * @return the assertion + */ + public Assertion newAssertion(final AuthenticationStatement authnStatement, final String issuer, + final DateTime issuedAt, final String id) { + final Assertion assertion = newSamlObject(Assertion.class); + assertion.setID(id); + assertion.setIssueInstant(issuedAt); + assertion.setIssuer(issuer); + assertion.getAuthenticationStatements().add(authnStatement); + return assertion; + } + + /** + * New conditions element. + * + * @param issuedAt the issued at + * @param audienceUri the service id + * @param issueLength the issue length + * @return the conditions + */ + public Conditions newConditions(final DateTime issuedAt, final String audienceUri, final long issueLength) { + final Conditions conditions = newSamlObject(Conditions.class); + conditions.setNotBefore(issuedAt); + conditions.setNotOnOrAfter(issuedAt.plus(issueLength)); + final AudienceRestrictionCondition audienceRestriction = newSamlObject(AudienceRestrictionCondition.class); + final Audience audience = newSamlObject(Audience.class); + audience.setUri(audienceUri); + audienceRestriction.getAudiences().add(audience); + conditions.getAudienceRestrictionConditions().add(audienceRestriction); + return conditions; + } + + /** + * Create a new SAML status object. + * + * @param codeValue the code value + * @param statusMessage the status message + * @return the status + */ + public Status newStatus(final QName codeValue, final String statusMessage) { + final Status status = newSamlObject(Status.class); + final StatusCode code = newSamlObject(StatusCode.class); + code.setValue(codeValue); + status.setStatusCode(code); + if (statusMessage != null) { + final StatusMessage message = newSamlObject(StatusMessage.class); + message.setMessage(statusMessage); + status.setStatusMessage(message); + } + return status; + } + + /** + * New authentication statement. + * + * @param authenticationDate the authentication date + * @param authenticationMethod the authentication method + * @param subjectId the subject id + * @return the authentication statement + */ + public AuthenticationStatement newAuthenticationStatement(final Date authenticationDate, + final String authenticationMethod, + final String subjectId) { + + final AuthenticationStatement authnStatement = newSamlObject(AuthenticationStatement.class); + authnStatement.setAuthenticationInstant(new DateTime(authenticationDate)); + authnStatement.setAuthenticationMethod( + authenticationMethod != null + ? authenticationMethod + : SamlAuthenticationMetaDataPopulator.AUTHN_METHOD_UNSPECIFIED); + authnStatement.setSubject(newSubject(subjectId)); + return authnStatement; + } + + /** + * New subject element that uses the confirmation method + * {@link #CONFIRMATION_METHOD}. + * + * @param identifier the identifier + * @return the subject + */ + public Subject newSubject(final String identifier) { + return newSubject(identifier, CONFIRMATION_METHOD); + } + + /** + * New subject element with given confirmation method. + * + * @param identifier the identifier + * @param confirmationMethod the confirmation method + * @return the subject + */ + public Subject newSubject(final String identifier, final String confirmationMethod) { + final SubjectConfirmation confirmation = newSamlObject(SubjectConfirmation.class); + final ConfirmationMethod method = newSamlObject(ConfirmationMethod.class); + method.setConfirmationMethod(confirmationMethod); + confirmation.getConfirmationMethods().add(method); + final NameIdentifier nameIdentifier = newSamlObject(NameIdentifier.class); + nameIdentifier.setNameIdentifier(identifier); + final Subject subject = newSamlObject(Subject.class); + subject.setNameIdentifier(nameIdentifier); + subject.setSubjectConfirmation(confirmation); + return subject; + } + + /** + * New attribute statement. + * + * @param subject the subject + * @param attributes the attributes + * @param attributeNamespace the attribute namespace + * @return the attribute statement + */ + public AttributeStatement newAttributeStatement(final Subject subject, + final Map attributes, + final String attributeNamespace) { + + final AttributeStatement attrStatement = newSamlObject(AttributeStatement.class); + attrStatement.setSubject(subject); + for (final Map.Entry e : attributes.entrySet()) { + if (e.getValue() instanceof Collection && ((Collection) e.getValue()).isEmpty()) { + // bnoordhuis: don't add the attribute, it causes a org.opensaml.MalformedException + logger.info("Skipping attribute {} because it does not have any values.", e.getKey()); + continue; + } + final Attribute attribute = newSamlObject(Attribute.class); + attribute.setAttributeName(e.getKey()); + attribute.setAttributeNamespace(attributeNamespace); + if (e.getValue() instanceof Collection) { + final Collection c = (Collection) e.getValue(); + for (final Object value : c) { + attribute.getAttributeValues().add(newAttributeValue(value, AttributeValue.DEFAULT_ELEMENT_NAME)); + } + } else { + attribute.getAttributeValues().add(newAttributeValue(e.getValue(), AttributeValue.DEFAULT_ELEMENT_NAME)); + } + attrStatement.getAttributes().add(attribute); + } + + return attrStatement; + } + + /** + * Encode response and pass it onto the outbound transport. + * Uses {@link CasHTTPSOAP11Encoder} to handle encoding. + * + * @param httpResponse the http response + * @param httpRequest the http request + * @param samlMessage the saml response + * @throws Exception the exception in case encoding fails. + */ + public void encodeSamlResponse(final HttpServletResponse httpResponse, + final HttpServletRequest httpRequest, + final Response samlMessage) throws Exception { + final BasicSAMLMessageContext messageContext = new BasicSAMLMessageContext(); + messageContext.setOutboundMessageTransport( + new HttpServletResponseAdapter(httpResponse, httpRequest.isSecure())); + messageContext.setOutboundSAMLMessage(samlMessage); + this.encoder.encode(messageContext); + } +} diff --git a/cas-server-support-saml/src/main/java/org/jasig/cas/support/saml/web/view/AbstractSaml10ResponseView.java b/cas-server-support-saml/src/main/java/org/jasig/cas/support/saml/web/view/AbstractSaml10ResponseView.java index f2012168b15a..f2c1d9eeaf4c 100644 --- a/cas-server-support-saml/src/main/java/org/jasig/cas/support/saml/web/view/AbstractSaml10ResponseView.java +++ b/cas-server-support-saml/src/main/java/org/jasig/cas/support/saml/web/view/AbstractSaml10ResponseView.java @@ -19,32 +19,17 @@ package org.jasig.cas.support.saml.web.view; import org.jasig.cas.authentication.principal.WebApplicationService; -import org.jasig.cas.support.saml.authentication.principal.SamlService; -import org.jasig.cas.support.saml.util.CasHTTPSOAP11Encoder; +import org.jasig.cas.support.saml.util.Saml10ObjectBuilder; import org.jasig.cas.support.saml.web.support.SamlArgumentExtractor; import org.jasig.cas.web.view.AbstractCasView; import org.joda.time.DateTime; -import org.opensaml.Configuration; import org.opensaml.DefaultBootstrap; -import org.opensaml.common.SAMLObject; -import org.opensaml.common.SAMLObjectBuilder; -import org.opensaml.common.SAMLVersion; -import org.opensaml.common.binding.BasicSAMLMessageContext; -import org.opensaml.common.impl.SecureRandomIdentifierGenerator; -import org.opensaml.saml1.binding.encoding.HTTPSOAP11Encoder; import org.opensaml.saml1.core.Response; -import org.opensaml.saml1.core.Status; -import org.opensaml.saml1.core.StatusCode; -import org.opensaml.saml1.core.StatusMessage; -import org.opensaml.ws.transport.http.HttpServletResponseAdapter; import org.opensaml.xml.ConfigurationException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.validation.constraints.NotNull; -import javax.xml.namespace.QName; -import java.lang.reflect.Field; -import java.security.NoSuchAlgorithmException; import java.util.Map; /** @@ -54,17 +39,14 @@ * @since 3.5.1 */ public abstract class AbstractSaml10ResponseView extends AbstractCasView { - - private static final String DEFAULT_ELEMENT_NAME_FIELD = "DEFAULT_ELEMENT_NAME"; - private static final String DEFAULT_ENCODING = "UTF-8"; - private final SamlArgumentExtractor samlArgumentExtractor = new SamlArgumentExtractor(); - - private final HTTPSOAP11Encoder encoder = new CasHTTPSOAP11Encoder(); - - private final SecureRandomIdentifierGenerator idGenerator; + /** + * The Saml object builder. + */ + protected final Saml10ObjectBuilder samlObjectBuilder = new Saml10ObjectBuilder(); + private final SamlArgumentExtractor samlArgumentExtractor = new SamlArgumentExtractor(); @NotNull private String encoding = DEFAULT_ENCODING; @@ -74,25 +56,12 @@ public abstract class AbstractSaml10ResponseView extends AbstractCasView { static { try { - // Initialize OpenSAML default configuration - // (only needed once per classloader) DefaultBootstrap.bootstrap(); } catch (final ConfigurationException e) { throw new IllegalStateException("Error initializing OpenSAML library.", e); } } - /** - * Instantiates a new abstract saml10 response view. - */ - protected AbstractSaml10ResponseView() { - try { - this.idGenerator = new SecureRandomIdentifierGenerator(); - } catch (final NoSuchAlgorithmException e) { - throw new IllegalStateException("Cannot create secure random ID generator for SAML message IDs."); - } - } - /** * Sets the character encoding in the HTTP response. * @@ -102,8 +71,6 @@ public void setEncoding(final String encoding) { this.encoding = encoding; } - - /** * Sets the allowance for time skew in seconds * between CAS and the client server. Default 0s. @@ -139,24 +106,13 @@ protected void renderMergedOutputModel( final String serviceId = service != null ? service.getId() : "UNKNOWN"; try { - final Response samlResponse = newSamlObject(Response.class); - samlResponse.setID(generateId()); - samlResponse.setIssueInstant(DateTime.now().minusSeconds(skewAllowance)); - samlResponse.setVersion(SAMLVersion.VERSION_11); - samlResponse.setRecipient(serviceId); - if (service instanceof SamlService) { - final SamlService samlService = (SamlService) service; - - if (samlService.getRequestID() != null) { - samlResponse.setInResponseTo(samlService.getRequestID()); - } - } + final Response samlResponse = this.samlObjectBuilder.newResponse( + this.samlObjectBuilder.generateSecureRandomId(), + DateTime.now().minusSeconds(this.skewAllowance), serviceId, service); + prepareResponse(samlResponse, model); - final BasicSAMLMessageContext messageContext = new BasicSAMLMessageContext(); - messageContext.setOutboundMessageTransport(new HttpServletResponseAdapter(response, request.isSecure())); - messageContext.setOutboundSAMLMessage(samlResponse); - this.encoder.encode(messageContext); + this.samlObjectBuilder.encodeSamlResponse(response, request, samlResponse); } catch (final Exception e) { logger.error("Error generating SAML response for service {}.", serviceId); throw e; @@ -172,57 +128,4 @@ protected void renderMergedOutputModel( */ protected abstract void prepareResponse(Response response, Map model); - - /** - * Generate id. - * - * @return the string - */ - protected final String generateId() { - return this.idGenerator.generateIdentifier(); - } - - /** - * Create a new SAML object. - * - * @param the generic type - * @param objectType the object type - * @return the t - */ - protected final T newSamlObject(final Class objectType) { - final QName qName; - try { - final Field f = objectType.getField(DEFAULT_ELEMENT_NAME_FIELD); - qName = (QName) f.get(null); - } catch (final NoSuchFieldException e) { - throw new IllegalStateException("Cannot find field " + objectType.getName() + "." + DEFAULT_ELEMENT_NAME_FIELD); - } catch (final IllegalAccessException e) { - throw new IllegalStateException("Cannot access field " + objectType.getName() + "." + DEFAULT_ELEMENT_NAME_FIELD); - } - final SAMLObjectBuilder builder = (SAMLObjectBuilder) Configuration.getBuilderFactory().getBuilder(qName); - if (builder == null) { - throw new IllegalStateException("No SAMLObjectBuilder registered for class " + objectType.getName()); - } - return objectType.cast(builder.buildObject()); - } - - /** - * Create a new SAML status object. - * - * @param codeValue the code value - * @param statusMessage the status message - * @return the status - */ - protected final Status newStatus(final QName codeValue, final String statusMessage) { - final Status status = newSamlObject(Status.class); - final StatusCode code = newSamlObject(StatusCode.class); - code.setValue(codeValue); - status.setStatusCode(code); - if (statusMessage != null) { - final StatusMessage message = newSamlObject(StatusMessage.class); - message.setMessage(statusMessage); - status.setStatusMessage(message); - } - return status; - } } diff --git a/cas-server-support-saml/src/main/java/org/jasig/cas/support/saml/web/view/Saml10FailureResponseView.java b/cas-server-support-saml/src/main/java/org/jasig/cas/support/saml/web/view/Saml10FailureResponseView.java index 054007bff8ca..800e1345a205 100644 --- a/cas-server-support-saml/src/main/java/org/jasig/cas/support/saml/web/view/Saml10FailureResponseView.java +++ b/cas-server-support-saml/src/main/java/org/jasig/cas/support/saml/web/view/Saml10FailureResponseView.java @@ -18,11 +18,11 @@ */ package org.jasig.cas.support.saml.web.view; -import java.util.Map; - import org.opensaml.saml1.core.Response; import org.opensaml.saml1.core.StatusCode; +import java.util.Map; + /** * Represents a failed attempt at validating a ticket, responding via a SAML SOAP message. * @@ -34,6 +34,6 @@ public final class Saml10FailureResponseView extends AbstractSaml10ResponseView @Override protected void prepareResponse(final Response response, final Map model) { - response.setStatus(newStatus(StatusCode.REQUEST_DENIED, (String) model.get("description"))); + response.setStatus(this.samlObjectBuilder.newStatus(StatusCode.REQUEST_DENIED, (String) model.get("description"))); } } diff --git a/cas-server-support-saml/src/main/java/org/jasig/cas/support/saml/web/view/Saml10SuccessResponseView.java b/cas-server-support-saml/src/main/java/org/jasig/cas/support/saml/web/view/Saml10SuccessResponseView.java index bd2ca6dbab37..915410c8ba32 100644 --- a/cas-server-support-saml/src/main/java/org/jasig/cas/support/saml/web/view/Saml10SuccessResponseView.java +++ b/cas-server-support-saml/src/main/java/org/jasig/cas/support/saml/web/view/Saml10SuccessResponseView.java @@ -25,28 +25,16 @@ import org.jasig.cas.support.saml.authentication.SamlAuthenticationMetaDataPopulator; import org.joda.time.DateTime; import org.opensaml.saml1.core.Assertion; -import org.opensaml.saml1.core.Attribute; -import org.opensaml.saml1.core.AttributeStatement; -import org.opensaml.saml1.core.AttributeValue; -import org.opensaml.saml1.core.Audience; -import org.opensaml.saml1.core.AudienceRestrictionCondition; import org.opensaml.saml1.core.AuthenticationStatement; import org.opensaml.saml1.core.Conditions; -import org.opensaml.saml1.core.ConfirmationMethod; -import org.opensaml.saml1.core.NameIdentifier; import org.opensaml.saml1.core.Response; import org.opensaml.saml1.core.StatusCode; import org.opensaml.saml1.core.Subject; -import org.opensaml.saml1.core.SubjectConfirmation; -import org.opensaml.xml.schema.XSString; -import org.opensaml.xml.schema.impl.XSStringBuilder; import javax.validation.constraints.Min; import javax.validation.constraints.NotNull; -import java.util.Collection; import java.util.HashMap; import java.util.Map; -import java.util.Map.Entry; /** * Implementation of a view to return a SAML SOAP response and assertion, based on @@ -67,12 +55,8 @@ public final class Saml10SuccessResponseView extends AbstractSaml10ResponseView /** Namespace for custom attributes in the saml validation payload. */ private static final String VALIDATION_SAML_ATTRIBUTE_NAMESPACE = "http://www.ja-sig.org/products/cas/"; - private static final String CONFIRMATION_METHOD = "urn:oasis:names:tc:SAML:1.0:cm:artifact"; - private static final int DEFAULT_ISSUE_LENGTH = 30000; - private final XSStringBuilder attrValueBuilder = new XSStringBuilder(); - /** The issuer, generally the hostname. */ @NotNull private String issuer; @@ -93,23 +77,27 @@ protected void prepareResponse(final Response response, final Map attributesToSend = prepareSamlAttributes(model); if (!attributesToSend.isEmpty()) { - assertion.getAttributeStatements().add(newAttributeStatement(subject, attributesToSend)); + assertion.getAttributeStatements().add(this.samlObjectBuilder.newAttributeStatement( + subject, attributesToSend, VALIDATION_SAML_ATTRIBUTE_NAMESPACE)); } - response.setStatus(newStatus(StatusCode.SUCCESS, null)); + response.setStatus(this.samlObjectBuilder.newStatus(StatusCode.SUCCESS, null)); response.getAssertions().add(assertion); } @@ -124,7 +112,8 @@ protected void prepareResponse(final Response response, final Map prepareSamlAttributes(final Map model) { - final Map authnAttributes = new HashMap(getAuthenticationAttributesAsMultiValuedAttributes(model)); + final Map authnAttributes = + new HashMap(getAuthenticationAttributesAsMultiValuedAttributes(model)); if (isRememberMeAuthentication(model)) { authnAttributes.remove(RememberMeCredential.AUTHENTICATION_ATTRIBUTE_REMEMBER_ME); authnAttributes.put(this.rememberMeAttributeName, Boolean.TRUE.toString()); @@ -135,115 +124,6 @@ private Map prepareSamlAttributes(final Map mode return attributesToReturn; } - /** - * New conditions element. - * - * @param issuedAt the issued at - * @param serviceId the service id - * @return the conditions - */ - private Conditions newConditions(final DateTime issuedAt, final String serviceId) { - final Conditions conditions = newSamlObject(Conditions.class); - conditions.setNotBefore(issuedAt); - conditions.setNotOnOrAfter(issuedAt.plus(this.issueLength)); - final AudienceRestrictionCondition audienceRestriction = newSamlObject(AudienceRestrictionCondition.class); - final Audience audience = newSamlObject(Audience.class); - audience.setUri(serviceId); - audienceRestriction.getAudiences().add(audience); - conditions.getAudienceRestrictionConditions().add(audienceRestriction); - return conditions; - } - - /** - * New subject element. - * - * @param identifier the identifier - * @return the subject - */ - private Subject newSubject(final String identifier) { - final SubjectConfirmation confirmation = newSamlObject(SubjectConfirmation.class); - final ConfirmationMethod method = newSamlObject(ConfirmationMethod.class); - method.setConfirmationMethod(CONFIRMATION_METHOD); - confirmation.getConfirmationMethods().add(method); - final NameIdentifier nameIdentifier = newSamlObject(NameIdentifier.class); - nameIdentifier.setNameIdentifier(identifier); - final Subject subject = newSamlObject(Subject.class); - subject.setNameIdentifier(nameIdentifier); - subject.setSubjectConfirmation(confirmation); - return subject; - } - - /** - * New authentication statement. - * - * @param model the model - * @return the authentication statement - */ - private AuthenticationStatement newAuthenticationStatement(final Map model) { - final Authentication authentication = getPrimaryAuthenticationFrom(model); - final String authenticationMethod = (String) authentication.getAttributes().get( - SamlAuthenticationMetaDataPopulator.ATTRIBUTE_AUTHENTICATION_METHOD); - final AuthenticationStatement authnStatement = newSamlObject(AuthenticationStatement.class); - authnStatement.setAuthenticationInstant(new DateTime(authentication.getAuthenticationDate())); - authnStatement.setAuthenticationMethod( - authenticationMethod != null - ? authenticationMethod - : SamlAuthenticationMetaDataPopulator.AUTHN_METHOD_UNSPECIFIED); - authnStatement.setSubject(newSubject(getPrincipal(model).getId())); - return authnStatement; - } - - /** - * New attribute statement. - * - * @param subject the subject - * @param attributes the attributes - * @return the attribute statement - */ - private AttributeStatement newAttributeStatement( - final Subject subject, final Map attributes) { - - final AttributeStatement attrStatement = newSamlObject(AttributeStatement.class); - attrStatement.setSubject(subject); - for (final Entry e : attributes.entrySet()) { - if (e.getValue() instanceof Collection && ((Collection) e.getValue()).isEmpty()) { - // bnoordhuis: don't add the attribute, it causes a org.opensaml.MalformedException - logger.info("Skipping attribute {} because it does not have any values.", e.getKey()); - continue; - } - final Attribute attribute = newSamlObject(Attribute.class); - attribute.setAttributeName(e.getKey()); - attribute.setAttributeNamespace(VALIDATION_SAML_ATTRIBUTE_NAMESPACE); - if (e.getValue() instanceof Collection) { - final Collection c = (Collection) e.getValue(); - for (final Object value : c) { - attribute.getAttributeValues().add(newAttributeValue(value)); - } - } else { - attribute.getAttributeValues().add(newAttributeValue(e.getValue())); - } - attrStatement.getAttributes().add(attribute); - } - - return attrStatement; - } - - /** - * New attribute value. - * - * @param value the value - * @return the xS string - */ - private XSString newAttributeValue(final Object value) { - final XSString stringValue = this.attrValueBuilder.buildObject(AttributeValue.DEFAULT_ELEMENT_NAME, XSString.TYPE_NAME); - if (value instanceof String) { - stringValue.setValue((String) value); - } else { - stringValue.setValue(value.toString()); - } - return stringValue; - } - public void setIssueLength(final long issueLength) { this.issueLength = issueLength; } diff --git a/cas-server-support-saml/src/test/java/org/jasig/cas/support/saml/authentication/principal/GoogleAccountsServiceTests.java b/cas-server-support-saml/src/test/java/org/jasig/cas/support/saml/authentication/principal/GoogleAccountsServiceTests.java index 87048f623656..5ae85a35f4e0 100644 --- a/cas-server-support-saml/src/test/java/org/jasig/cas/support/saml/authentication/principal/GoogleAccountsServiceTests.java +++ b/cas-server-support-saml/src/test/java/org/jasig/cas/support/saml/authentication/principal/GoogleAccountsServiceTests.java @@ -18,32 +18,40 @@ */ package org.jasig.cas.support.saml.authentication.principal; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.security.interfaces.DSAPrivateKey; -import java.security.interfaces.DSAPublicKey; -import java.util.zip.DeflaterOutputStream; - import org.apache.commons.codec.binary.Base64; import org.jasig.cas.TestUtils; +import org.jasig.cas.authentication.principal.Response; import org.jasig.cas.authentication.principal.Service; import org.jasig.cas.services.DefaultRegisteredServiceUsernameProvider; import org.jasig.cas.services.RegisteredService; import org.jasig.cas.services.ServicesManager; +import org.jasig.cas.support.saml.SamlProtocolConstants; import org.jasig.cas.util.PrivateKeyFactoryBean; import org.jasig.cas.util.PublicKeyFactoryBean; import org.junit.Before; import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.core.io.ClassPathResource; import org.springframework.mock.web.MockHttpServletRequest; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.security.interfaces.DSAPrivateKey; +import java.security.interfaces.DSAPublicKey; +import java.util.zip.DeflaterOutputStream; + import static org.mockito.Mockito.*; +import static org.junit.Assert.*; + /** * @author Scott Battaglia * @since 3.1 */ public class GoogleAccountsServiceTests { + private final Logger logger = LoggerFactory.getLogger(this.getClass()); + private GoogleAccountsService googleAccountsService; public static GoogleAccountsService getGoogleAccountsService() throws Exception { @@ -70,7 +78,8 @@ public static GoogleAccountsService getGoogleAccountsService() throws Exception + "ID=\"5545454455\" Version=\"2.0\" IssueInstant=\"Value\" " + "ProtocolBinding=\"urn:oasis:names.tc:SAML:2.0:bindings:HTTP-Redirect\" " + "ProviderName=\"https://localhost:8443/myRutgers\" AssertionConsumerServiceURL=\"https://localhost:8443/myRutgers\"/>"; - request.setParameter("SAMLRequest", encodeMessage(samlRequest)); + request.setParameter(SamlProtocolConstants.PARAMETER_SAML_REQUEST, encodeMessage(samlRequest)); + request.setParameter(SamlProtocolConstants.PARAMETER_SAML_RELAY_STATE, "RelayStateAddedHere"); final RegisteredService regSvc = mock(RegisteredService.class); when(regSvc.getUsernameAttributeProvider()).thenReturn(new DefaultRegisteredServiceUsernameProvider()); @@ -87,22 +96,20 @@ public void setUp() throws Exception { this.googleAccountsService.setPrincipal(TestUtils.getPrincipal()); } - - // XXX: re-enable when we figure out JVM requirements @Test public void verifyResponse() { - return; - // final Response response = this.googleAccountsService.getResponse("ticketId"); - // assertEquals(ResponseType.POST, response.getResponseType()); - // assertTrue(response.getAttributes().containsKey("SAMLResponse")); + final Response resp = this.googleAccountsService.getResponse("ticketId"); + assertEquals(resp.getResponseType(), Response.ResponseType.POST); + assertTrue(resp.getAttributes().containsKey(SamlProtocolConstants.PARAMETER_SAML_RESPONSE)); + assertTrue(resp.getAttributes().containsKey(SamlProtocolConstants.PARAMETER_SAML_RELAY_STATE)); + } - protected static String encodeMessage(final String xmlString) throws IOException { + private static String encodeMessage(final String xmlString) throws IOException { final byte[] xmlBytes = xmlString.getBytes("UTF-8"); final ByteArrayOutputStream byteOutputStream = new ByteArrayOutputStream(); - final DeflaterOutputStream deflaterOutputStream = new DeflaterOutputStream( - byteOutputStream); + final DeflaterOutputStream deflaterOutputStream = new DeflaterOutputStream(byteOutputStream); deflaterOutputStream.write(xmlBytes, 0, xmlBytes.length); deflaterOutputStream.close();