diff --git a/.gitignore b/.gitignore index d140809..6774b28 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ target .project .classpath -.settings \ No newline at end of file +.settings +.DS_Store/ diff --git a/README.md b/README.md index e915c6b..8093bd5 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,8 @@ # Dead Simple SAML 2.0 Client +This is coveooss/saml-client with some crude alterations to allow more flexibility for setting keys where they aren't held in files. + This library implements a very simple SAML 2.0 client that allows retrieving an authenticated identity from a compliant identity provider, using the HTTP POST binding. It is based on the OpenSAML library, and only provides the necessary glue code to make it work in a basic scenario. This is by no means a complete implementation supporting all the nitty gritty SAML details, but it does perform the basic task of generating requests and validating responses. It's useful if you need to authenticate with SAML but don't want to bring in an uber large framework such as Spring Security. diff --git a/pom.xml b/pom.xml index 39d3015..a31f51d 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ com.coveo saml-client - 5.0.1-SNAPSHOT + 5.0.1 jar ${project.groupId}:${project.artifactId} diff --git a/src/main/java/com/coveo/saml/MetadataUtils.java b/src/main/java/com/coveo/saml/MetadataUtils.java index ac7ac4a..17d8a60 100644 --- a/src/main/java/com/coveo/saml/MetadataUtils.java +++ b/src/main/java/com/coveo/saml/MetadataUtils.java @@ -33,146 +33,147 @@ import org.slf4j.LoggerFactory; import org.w3c.dom.Document; - public class MetadataUtils { - private static final Logger logger = LoggerFactory.getLogger(SamlClient.class); - - public static String generateSpMetadata(String entityId, String assertionConsumerServiceURL, String logoutServiceURL) { - return generateSpMetadata(entityId, assertionConsumerServiceURL, logoutServiceURL, null); - } - - public static String generateSpMetadata(String entityId, String assertionConsumerServiceURL, String singleLogoutServiceURL, X509Certificate certificate) { - try { - InitializationService.initialize(); - - EntityDescriptor spEntityDescriptor = createSAMLObject(EntityDescriptor.class); - if (spEntityDescriptor == null) { - return null; - } - spEntityDescriptor.setEntityID(entityId); - SPSSODescriptor spSSODescriptor = createSAMLObject(SPSSODescriptor.class); - if (spSSODescriptor == null) { - return null; - } - - spSSODescriptor.setWantAssertionsSigned(false); - spSSODescriptor.setAuthnRequestsSigned(false); - - if (certificate != null) { - - spSSODescriptor.setWantAssertionsSigned(true); - spSSODescriptor.setAuthnRequestsSigned(true); - - X509KeyInfoGeneratorFactory keyInfoGeneratorFactory = new X509KeyInfoGeneratorFactory(); - keyInfoGeneratorFactory.setEmitEntityCertificate(true); - KeyInfoGenerator keyInfoGenerator = keyInfoGeneratorFactory.newInstance(); - - KeyDescriptor encKeyDescriptor = createSAMLObject(KeyDescriptor.class); - if (encKeyDescriptor == null) { - return null; - } - - encKeyDescriptor.setUse(UsageType.ENCRYPTION); - - Credential credential = new BasicX509Credential(certificate); - - try { - encKeyDescriptor.setKeyInfo(keyInfoGenerator.generate(credential)); - } - catch (Exception e) { - logger.error("Error while creating credentials", e); - } - spSSODescriptor.getKeyDescriptors().add(encKeyDescriptor); - - KeyDescriptor signKeyDescriptor = createSAMLObject(KeyDescriptor.class); - if (signKeyDescriptor == null) { - return null; - } - - signKeyDescriptor.setUse(UsageType.SIGNING); // Set usage - - try { - signKeyDescriptor.setKeyInfo(keyInfoGenerator.generate(credential)); - } - catch (SecurityException e) { - logger.error("Error while creating credentials", e); - } - spSSODescriptor.getKeyDescriptors().add(signKeyDescriptor); - } - - SingleLogoutService singleLogoutService = createSAMLObject(SingleLogoutService.class); - if (singleLogoutService == null) { - return null; - } - singleLogoutService.setBinding(SAMLConstants.SAML2_REDIRECT_BINDING_URI); - singleLogoutService.setLocation(singleLogoutServiceURL); - spSSODescriptor.getSingleLogoutServices().add(singleLogoutService); - - NameIDFormat nameIDFormat = createSAMLObject(NameIDFormat.class); - if (nameIDFormat == null) { - return null; - } - - nameIDFormat.setFormat("urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"); - spSSODescriptor.getNameIDFormats().add(nameIDFormat); - - AssertionConsumerService assertionConsumerService = createSAMLObject(AssertionConsumerService.class); - if (assertionConsumerService == null) { - return null; - } - assertionConsumerService.setIndex(1); - assertionConsumerService.setBinding(SAMLConstants.SAML2_POST_BINDING_URI); - - assertionConsumerService.setLocation(assertionConsumerServiceURL); - spSSODescriptor.getAssertionConsumerServices().add(assertionConsumerService); - - spSSODescriptor.addSupportedProtocol(SAMLConstants.SAML20P_NS); - - spEntityDescriptor.getRoleDescriptors().add(spSSODescriptor); - - DocumentBuilder builder; - DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); - factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); - - builder = factory.newDocumentBuilder(); - Document document = builder.newDocument(); - Marshaller out = XMLObjectProviderRegistrySupport.getMarshallerFactory().getMarshaller(spEntityDescriptor); - out.marshall(spEntityDescriptor, document); - - TransformerFactory transformerfactory = TransformerFactory.newInstance(); - transformerfactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); - Transformer transformer = transformerfactory.newTransformer(); - StringWriter stringWriter = new StringWriter(); - StreamResult streamResult = new StreamResult(stringWriter); - DOMSource source = new DOMSource(document); - transformer.setOutputProperty(OutputKeys.INDENT, "yes"); - transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4"); - transformer.transform(source, streamResult); - stringWriter.close(); - - return stringWriter.toString(); - } - catch (Exception e) { - logger.error("Error while generation SP metadata", e); - return null; - } - - } - - public static T createSAMLObject(final Class clazz) { - XMLObjectBuilderFactory builderFactory = XMLObjectProviderRegistrySupport.getBuilderFactory(); - - QName defaultElementName = null; - try { - defaultElementName = (QName) clazz.getDeclaredField("DEFAULT_ELEMENT_NAME").get(null); - } - catch (Exception e) { - logger.error("Error while creating SAML object", e); - return null; - } - T object = (T) builderFactory.getBuilder(defaultElementName).buildObject(defaultElementName); - - return object; - } + private static final Logger logger = LoggerFactory.getLogger(SamlClient.class); + + public static String generateSpMetadata( + String entityId, String assertionConsumerServiceURL, String logoutServiceURL) { + return generateSpMetadata(entityId, assertionConsumerServiceURL, logoutServiceURL, null); + } + + public static String generateSpMetadata( + String entityId, + String assertionConsumerServiceURL, + String singleLogoutServiceURL, + X509Certificate certificate) { + try { + InitializationService.initialize(); + + EntityDescriptor spEntityDescriptor = createSAMLObject(EntityDescriptor.class); + if (spEntityDescriptor == null) { + return null; + } + spEntityDescriptor.setEntityID(entityId); + SPSSODescriptor spSSODescriptor = createSAMLObject(SPSSODescriptor.class); + if (spSSODescriptor == null) { + return null; + } + + spSSODescriptor.setWantAssertionsSigned(false); + spSSODescriptor.setAuthnRequestsSigned(false); + + if (certificate != null) { + + spSSODescriptor.setWantAssertionsSigned(true); + spSSODescriptor.setAuthnRequestsSigned(true); + + X509KeyInfoGeneratorFactory keyInfoGeneratorFactory = new X509KeyInfoGeneratorFactory(); + keyInfoGeneratorFactory.setEmitEntityCertificate(true); + KeyInfoGenerator keyInfoGenerator = keyInfoGeneratorFactory.newInstance(); + + KeyDescriptor encKeyDescriptor = createSAMLObject(KeyDescriptor.class); + if (encKeyDescriptor == null) { + return null; + } + + encKeyDescriptor.setUse(UsageType.ENCRYPTION); + + Credential credential = new BasicX509Credential(certificate); + + try { + encKeyDescriptor.setKeyInfo(keyInfoGenerator.generate(credential)); + } catch (Exception e) { + logger.error("Error while creating credentials", e); + } + spSSODescriptor.getKeyDescriptors().add(encKeyDescriptor); + + KeyDescriptor signKeyDescriptor = createSAMLObject(KeyDescriptor.class); + if (signKeyDescriptor == null) { + return null; + } + + signKeyDescriptor.setUse(UsageType.SIGNING); // Set usage + + try { + signKeyDescriptor.setKeyInfo(keyInfoGenerator.generate(credential)); + } catch (SecurityException e) { + logger.error("Error while creating credentials", e); + } + spSSODescriptor.getKeyDescriptors().add(signKeyDescriptor); + } + + SingleLogoutService singleLogoutService = createSAMLObject(SingleLogoutService.class); + if (singleLogoutService == null) { + return null; + } + singleLogoutService.setBinding(SAMLConstants.SAML2_REDIRECT_BINDING_URI); + singleLogoutService.setLocation(singleLogoutServiceURL); + spSSODescriptor.getSingleLogoutServices().add(singleLogoutService); + + NameIDFormat nameIDFormat = createSAMLObject(NameIDFormat.class); + if (nameIDFormat == null) { + return null; + } + + nameIDFormat.setFormat("urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"); + spSSODescriptor.getNameIDFormats().add(nameIDFormat); + + AssertionConsumerService assertionConsumerService = + createSAMLObject(AssertionConsumerService.class); + if (assertionConsumerService == null) { + return null; + } + assertionConsumerService.setIndex(1); + assertionConsumerService.setBinding(SAMLConstants.SAML2_POST_BINDING_URI); + + assertionConsumerService.setLocation(assertionConsumerServiceURL); + spSSODescriptor.getAssertionConsumerServices().add(assertionConsumerService); + + spSSODescriptor.addSupportedProtocol(SAMLConstants.SAML20P_NS); + + spEntityDescriptor.getRoleDescriptors().add(spSSODescriptor); + + DocumentBuilder builder; + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); + + builder = factory.newDocumentBuilder(); + Document document = builder.newDocument(); + Marshaller out = + XMLObjectProviderRegistrySupport.getMarshallerFactory().getMarshaller(spEntityDescriptor); + out.marshall(spEntityDescriptor, document); + + TransformerFactory transformerfactory = TransformerFactory.newInstance(); + transformerfactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); + Transformer transformer = transformerfactory.newTransformer(); + StringWriter stringWriter = new StringWriter(); + StreamResult streamResult = new StreamResult(stringWriter); + DOMSource source = new DOMSource(document); + transformer.setOutputProperty(OutputKeys.INDENT, "yes"); + transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4"); + transformer.transform(source, streamResult); + stringWriter.close(); + + return stringWriter.toString(); + } catch (Exception e) { + logger.error("Error while generation SP metadata", e); + return null; + } + } + + public static T createSAMLObject(final Class clazz) { + XMLObjectBuilderFactory builderFactory = XMLObjectProviderRegistrySupport.getBuilderFactory(); + + QName defaultElementName = null; + try { + defaultElementName = (QName) clazz.getDeclaredField("DEFAULT_ELEMENT_NAME").get(null); + } catch (Exception e) { + logger.error("Error while creating SAML object", e); + return null; + } + T object = (T) builderFactory.getBuilder(defaultElementName).buildObject(defaultElementName); + + return object; + } } diff --git a/src/main/java/com/coveo/saml/SamlClient.java b/src/main/java/com/coveo/saml/SamlClient.java index 3eb3c27..a735f04 100644 --- a/src/main/java/com/coveo/saml/SamlClient.java +++ b/src/main/java/com/coveo/saml/SamlClient.java @@ -2,6 +2,7 @@ import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; @@ -10,8 +11,11 @@ import java.io.RandomAccessFile; import java.io.Reader; import java.io.StringWriter; +import java.net.URLEncoder; import java.nio.charset.StandardCharsets; +import java.security.InvalidKeyException; import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; @@ -26,6 +30,8 @@ import java.util.UUID; import java.util.stream.Collectors; import java.util.stream.Stream; +import java.util.zip.Deflater; +import java.util.zip.DeflaterOutputStream; import java.util.zip.Inflater; import java.util.zip.InflaterInputStream; @@ -98,6 +104,9 @@ import net.shibboleth.utilities.java.support.component.ComponentInitializationException; import net.shibboleth.utilities.java.support.xml.BasicParserPool; import net.shibboleth.utilities.java.support.xml.XMLParserException; +import org.opensaml.xmlsec.encryption.support.ChainingEncryptedKeyResolver; +import org.opensaml.xmlsec.encryption.support.EncryptedKeyResolver; +import org.opensaml.xmlsec.encryption.support.SimpleRetrievalMethodEncryptedKeyResolver; public class SamlClient { private static final Logger logger = LoggerFactory.getLogger(SamlClient.class); @@ -123,6 +132,7 @@ public enum SamlIdpBinding { private final SamlIdpBinding samlBinding; private BasicX509Credential spCredential; private List additionalSpCredentials = new ArrayList<>(); + private boolean forceAuthn; //2.34.1 C281958 /** * Returns the url where SAML requests should be posted. @@ -282,7 +292,12 @@ public SamlResponse decodeAndValidateSamlResponse(String encodedResponse, String throw new SamlException("Cannot decrypt the assertion", e); } //Validate the response (Assertion / Signature / Schema) - ValidatorUtils.validate(response, responseIssuer, credentials, this.now, notBeforeSkew); + ValidatorUtils.validate( + response, + responseIssuer, + credentials, + this.now, + notBeforeSkew); //If this fails with REQUESTER = "urn:oasis:names:tc:SAML:2.0:status:Requester"; then the IDP is saying that the request was wrong. Assertion assertion = response.getAssertions().get(0); return new SamlResponse(assertion); @@ -298,6 +313,12 @@ public SamlResponse decodeAndValidateSamlResponse(String encodedResponse, String */ public void redirectToIdentityProvider(HttpServletResponse response, String relayState) throws IOException, SamlException { + + if (samlBinding == SamlClient.SamlIdpBinding.Redirect) { + response.sendRedirect(getRedirectURL(relayState)); + return; + } + Map values = new HashMap<>(); values.put("SAMLRequest", getSamlRequest()); if (relayState != null) { @@ -307,6 +328,70 @@ public void redirectToIdentityProvider(HttpServletResponse response, String rela BrowserUtils.postUsingBrowser(identityProviderUrl, response, values); } + public String getRedirectURL(String relayState) throws IOException, SamlException { + //From https://medium.com/@sagarag/reloading-saml-saml-basics-b8999995c73e + //- RelayState is supported but should be limited to 80 bytes and it should be appended to the URL with “RelayState=value” format. + //- The SAML request is first compressed using DEFLATE compression mechanism and then encoded using Base64 finally it again need to be URL encoded. + //- For the request signing, it should use DSAWithSHA or RSAWithSHA algorithms. + //- HTTP Cache should not be used with this binding. + //- SAML processors should not use HTTP error codes instead SAML error coded need to be used. + // + //Following are the URL parameter names supported in this binding. + //- SAMLRequest — SAML request message + //- RelayState — RelayState value + //nb. these are additional to POST... + //- SAMLEncoding — indicate the SAML encoding mechanism + //- SigAlg — Signature algorithm identifier + //- Signature — Base64 encoded signature value + // + //Example: https://localhost:9443/samlsso?SAMLRequest=nZPBjpskDyneY4s...&SigAlg=http%3A%2F%2Fwww.w3.org%2F2000%2F09%2Fxmldsig%23rsa-sha1&Signature=VoTUhZQVI1y... + // + //From: https://www.oasis-open.org/committees/download.php/35387/sstc-saml-bindings-errata-2.0-wd-05-diff.pdf + //1. Any signature on the SAML protocol message, including the XML element itself,MUST be removed. + // Note that if the content of the message includes another signature, such as asigned SAML assertion, this embedded signature is not removed. + // However, the length of such amessage after encoding essentially precludes using this mechanism. + // Thus SAML protocolmessages that contain signed content SHOULD NOT be encoded using this mechanism. + //2. The DEFLATE compression mechanism, as specified in [RFC1951] is then applied to the entireremaining XML content of the original SAML protocol message. + //3. The compressed data is subsequently base64-encoded according to the rules specified in IETFRFC 2045 [RFC2045]. + // Linefeeds or other whitespace MUST be removed from the result. + //4. The base-64 encoded data is then URL-encoded, and added to the URL as a query stringparameter which MUST be named SAMLRequest (if the message is a SAML request) or + // SAMLResponse (if the message is a SAML response). + //5. If RelayState data is to accompany the SAML protocol message, it MUST be URL-encoded and placed in an additional query string parameter named RelayState. + //6. If the original SAML protocol message was signed using an XML digital signature, a new signaturecovering the encoded data as specified above MUST be attached using the rules stated below. + // + //SAMLRequest=value&RelayState=value&SigAlg=value "Finally, note that if there is no RelayState value, the entire parameter should be omitted from thesignature computation (and not included as an empty parameter name)." + // + StringBuilder sb = new StringBuilder(); + sb.append("SAMLRequest=") + .append(URLEncoder.encode(getSamlRequest(SamlClient.SamlIdpBinding.Redirect), "UTF-8")); + + if (relayState != null) + sb.append("&RelayState=").append(URLEncoder.encode(relayState, "UTF-8")); + + sb.append("&SigAlg=") + .append( + "http%3A%2F%2Fwww.w3.org%2F2001%2F04%2Fxmldsig-more%23rsa-sha256"); //http://www.w3.org/2001/04/xmldsig-more#rsa-sha256 + + byte[] bytesToSign = sb.toString().getBytes("UTF-8"); + try { + java.security.Signature sig = java.security.Signature.getInstance("SHA256withRSA"); + sig.initSign(spCredential.getPrivateKey()); + sig.update(bytesToSign); + byte[] signature = sig.sign(); + sb.append("&Signature=") + .append(URLEncoder.encode(Base64.encodeBase64String(signature), "UTF-8")); + } catch (NoSuchAlgorithmException | InvalidKeyException | java.security.SignatureException e) { + throw new SamlException("Unsupported algorithm. " + e); + } + + sb.append("&SAMLEncoding=") + .append( + URLEncoder.encode( + "urn:oasis:names:tc:SAML:2.0:bindings:URL-Encoding:DEFLATE", "UTF-8")); //optional + + return identityProviderUrl + "?" + sb.toString(); + } + /** * Processes a POST containing the SAML response. * @@ -738,9 +823,11 @@ private RequestAbstractType getBasicSamlRequest(QName defaultElementName) { /** Convert a SAML request to a base64-encoded String * * @param request The request to encode + * @param binding Whether we are doing a POST or Redirect * @throws SamlException if marshalling the request fails * */ - private String marshallAndEncodeSamlObject(RequestAbstractType request) throws SamlException { + private String marshallAndEncodeSamlObject(RequestAbstractType request, SamlIdpBinding binding) + throws SamlException { StringWriter stringWriter; try { stringWriter = marshallXmlObject(request); @@ -750,16 +837,50 @@ private String marshallAndEncodeSamlObject(RequestAbstractType request) throws S logger.trace("Issuing SAML request: " + stringWriter); - return Base64.encodeBase64String(stringWriter.toString().getBytes(StandardCharsets.UTF_8)); + byte[] requestBytes; + if (binding == SamlClient.SamlIdpBinding.Redirect) { + try { + String requestStr = stringWriter.toString(); + //For a Redirect we compress (deflate) the bytes before we Base64 them + InputStream input = new ByteArrayInputStream(requestStr.getBytes("UTF-8")); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + Deflater def = new Deflater(1, true); + DeflaterOutputStream compresser = new DeflaterOutputStream(output, def); + int len; + byte[] oneKay = new byte[1024]; + while ((len = input.read(oneKay)) != -1) { + compresser.write(oneKay, 0, len); + } + compresser.flush(); + compresser.finish(); + requestBytes = output.toByteArray(); + } catch (IOException e) { + throw new SamlException("Failed to deflate request for redirect.", e); + } + } else { + //For a POST we just Base64 the bytes + requestBytes = stringWriter.toString().getBytes(StandardCharsets.UTF_8); + } + return Base64.encodeBase64String(requestBytes); } /** - * Builds an encoded SAML request. + * Builds an encoded SAML request for the POST binding. * * @return The base-64 encoded SAML request. * @throws SamlException thrown if an unexpected error occurs. */ public String getSamlRequest() throws SamlException { + return getSamlRequest(SamlClient.SamlIdpBinding.POST); + } + + /** + * Builds an encoded SAML request. + * + * @return The base-64 encoded SAML request. + * @throws SamlException thrown if an unexpected error occurs. + */ + public String getSamlRequest(SamlIdpBinding binding) throws SamlException { AuthnRequest request = (AuthnRequest) getBasicSamlRequest(AuthnRequest.DEFAULT_ELEMENT_NAME); request.setProtocolBinding( @@ -771,14 +892,19 @@ public String getSamlRequest() throws SamlException { nameIDPolicy.setFormat("urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"); request.setNameIDPolicy(nameIDPolicy); - signSAMLObject(request); + //2.34.1 C281958 + if (forceAuthn) request.setForceAuthn(true); - return marshallAndEncodeSamlObject(request); + if (binding != SamlClient.SamlIdpBinding.Redirect) signSAMLObject(request); + + return marshallAndEncodeSamlObject(request, binding); } /** * Gets the encoded logout request. * + * Assumes use of POST. + * * @param nameId the name id * @return the logout request * @throws SamlException the saml exception @@ -792,7 +918,7 @@ public String getLogoutRequest(String nameId) throws SamlException { signSAMLObject(request); - return marshallAndEncodeSamlObject(request); + return marshallAndEncodeSamlObject(request, SamlClient.SamlIdpBinding.POST); } /** * Gets saml logout response. @@ -941,11 +1067,17 @@ private void decodeEncryptedAssertion(Response response) throws DecryptionExcept resolverChain.add(new CollectionKeyInfoCredentialResolver(additionalSpCredentials)); } + //4.1.2-datb-2 A275378 If encrypted assertions use RetrievalMethod to refer to an EncryptedKey using a URI within the + //document then SimpleRetrievalMethodEncryptedKeyResolver is needed to resolve these. + List ekResolverList = new ArrayList<>(); + ekResolverList.add(new InlineEncryptedKeyResolver()); + ekResolverList.add(new SimpleRetrievalMethodEncryptedKeyResolver()); + Decrypter decrypter = new Decrypter( null, new ChainingKeyInfoCredentialResolver(resolverChain), - new InlineEncryptedKeyResolver()); + new ChainingEncryptedKeyResolver(ekResolverList)); decrypter.setRootInNewDocument(true); @@ -975,6 +1107,22 @@ private X509Certificate loadCertificate(String filename) throws SamlException { } } + /** + * Load an X.509 certificate + * @param bis An input stream providing the bytes of the key. + * */ + private X509Certificate loadCertificate(InputStream bis) throws SamlException { + try { + + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + + return (X509Certificate) cf.generateCertificate(bis); + + } catch (Exception e) { + throw new SamlException("Couldn't load public key", e); + } + } + /** * Load a PKCS8 key * @param filename The path of the key @@ -995,6 +1143,40 @@ private PrivateKey loadPrivateKey(String filename) throws SamlException { } } + /** + * Load a PKCS8 key + * @param bis An input stream providing the bytes of the key. + * */ + private PrivateKey loadPrivateKey(InputStream bis) throws SamlException { + try //(RandomAccessFile raf = new RandomAccessFile(filename, "r")) + { + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + + int totalBytes = 0; + byte[] onekay = new byte[1024]; + int bytesRead = 0; + while (true) { + bytesRead = bis.read(onekay); + + if (bytesRead == -1) break; + + if (bytesRead > 0) baos.write(onekay, 0, bytesRead); + } + bis.close(); + baos.flush(); + baos.close(); + + PKCS8EncodedKeySpec kspec = new PKCS8EncodedKeySpec(baos.toByteArray()); + KeyFactory kf = KeyFactory.getInstance("RSA"); + + return kf.generatePrivate(kspec); + + } catch (Exception e) { + throw new SamlException("Couldn't load private key", e); + } + } + private StringWriter marshallXmlObject(XMLObject object) throws MarshallingException { StringWriter stringWriter = new StringWriter(); Marshaller marshaller = @@ -1050,4 +1232,22 @@ private void signSAMLObject(SignableSAMLObject samlObject) throws SamlException } } } + + /** + * Get the response issuer. + * + * @return + */ + public String getResponseIssuer() { + return responseIssuer; + } + + /** + * Set whether to set the forceAuthn flag on any SamlRequests. + * + * 2.34.1 C281958 + */ + public void setForceAuthn(boolean forceAuthn) { + this.forceAuthn = forceAuthn; + } } diff --git a/src/test/java/com/coveo/saml/MetadataUtilsTest.java b/src/test/java/com/coveo/saml/MetadataUtilsTest.java index fe20d3f..f013a32 100644 --- a/src/test/java/com/coveo/saml/MetadataUtilsTest.java +++ b/src/test/java/com/coveo/saml/MetadataUtilsTest.java @@ -10,43 +10,48 @@ import org.junit.Test; - public class MetadataUtilsTest { - @Test - public void generateSpMetadata_AllNull() { - String metadata = MetadataUtils.generateSpMetadata(null, null, null); - assertEquals( - " urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified ", - metadata.replace("\r\n", "").replace("\n", "")); - } - - @Test - public void generateSpMetadata_AllFields() { - String metadata = MetadataUtils.generateSpMetadata("testSp", "http://localhost:8080/consume", "http://localhost:8080/logout"); - assertEquals( - " urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified ", - metadata.replace("\r\n", "").replace("\n", "")); - } - - @Test - public void generateSpMetadata_AllFieldsAndCertificat() { - Certificate cert = null; - try { - InputStream keyStoreInputStream = this.getClass().getResourceAsStream("/com/coveo/saml/test.p12"); - - KeyStore keystore = KeyStore.getInstance("PKCS12"); - keystore.load(keyStoreInputStream, "test".toCharArray()); - cert = keystore.getCertificate("tester"); - } - catch (Exception e) { - fail(); - } - - String metadata = MetadataUtils.generateSpMetadata("testSp", "http://localhost:8080/consume", "http://localhost:8080/logout", (X509Certificate) cert); - assertEquals( - " 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 urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified ", - metadata.replace("\r\n", "").replace("\n", "")); - } - + @Test + public void generateSpMetadata_AllNull() { + String metadata = MetadataUtils.generateSpMetadata(null, null, null); + assertEquals( + " urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified ", + metadata.replace("\r\n", "").replace("\n", "")); + } + + @Test + public void generateSpMetadata_AllFields() { + String metadata = + MetadataUtils.generateSpMetadata( + "testSp", "http://localhost:8080/consume", "http://localhost:8080/logout"); + assertEquals( + " urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified ", + metadata.replace("\r\n", "").replace("\n", "")); + } + + @Test + public void generateSpMetadata_AllFieldsAndCertificat() { + Certificate cert = null; + try { + InputStream keyStoreInputStream = + this.getClass().getResourceAsStream("/com/coveo/saml/test.p12"); + + KeyStore keystore = KeyStore.getInstance("PKCS12"); + keystore.load(keyStoreInputStream, "test".toCharArray()); + cert = keystore.getCertificate("tester"); + } catch (Exception e) { + fail(); + } + + String metadata = + MetadataUtils.generateSpMetadata( + "testSp", + "http://localhost:8080/consume", + "http://localhost:8080/logout", + (X509Certificate) cert); + assertEquals( + " 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 urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified ", + metadata.replace("\r\n", "").replace("\n", "")); + } } diff --git a/src/test/resources/com/coveo/saml/samltest.id/idp.xml b/src/test/resources/com/coveo/saml/samltest.id/idp.xml new file mode 100644 index 0000000..861829a --- /dev/null +++ b/src/test/resources/com/coveo/saml/samltest.id/idp.xml @@ -0,0 +1,122 @@ + + + + + + + + + samltest.id + + + + SAMLtest IdP + A free and basic IdP for testing SAML deployments + https://samltest.id/saml/logo.png + + + + + + + +MIIDETCCAfmgAwIBAgIUZRpDhkNKl5eWtJqk0Bu1BgTTargwDQYJKoZIhvcNAQEL +BQAwFjEUMBIGA1UEAwwLc2FtbHRlc3QuaWQwHhcNMTgwODI0MjExNDEwWhcNMzgw +ODI0MjExNDEwWjAWMRQwEgYDVQQDDAtzYW1sdGVzdC5pZDCCASIwDQYJKoZIhvcN +AQEBBQADggEPADCCAQoCggEBAJrh9/PcDsiv3UeL8Iv9rf4WfLPxuOm9W6aCntEA +8l6c1LQ1Zyrz+Xa/40ZgP29ENf3oKKbPCzDcc6zooHMji2fBmgXp6Li3fQUzu7yd ++nIC2teejijVtrNLjn1WUTwmqjLtuzrKC/ePoZyIRjpoUxyEMJopAd4dJmAcCq/K +k2eYX9GYRlqvIjLFoGNgy2R4dWwAKwljyh6pdnPUgyO/WjRDrqUBRFrLQJorR2kD +c4seZUbmpZZfp4MjmWMDgyGM1ZnR0XvNLtYeWAyt0KkSvFoOMjZUeVK/4xR74F8e +8ToPqLmZEg9ZUx+4z2KjVK00LpdRkH9Uxhh03RQ0FabHW6UCAwEAAaNXMFUwHQYD +VR0OBBYEFJDbe6uSmYQScxpVJhmt7PsCG4IeMDQGA1UdEQQtMCuCC3NhbWx0ZXN0 +LmlkhhxodHRwczovL3NhbWx0ZXN0LmlkL3NhbWwvaWRwMA0GCSqGSIb3DQEBCwUA +A4IBAQBNcF3zkw/g51q26uxgyuy4gQwnSr01Mhvix3Dj/Gak4tc4XwvxUdLQq+jC +cxr2Pie96klWhY/v/JiHDU2FJo9/VWxmc/YOk83whvNd7mWaNMUsX3xGv6AlZtCO +L3JhCpHjiN+kBcMgS5jrtGgV1Lz3/1zpGxykdvS0B4sPnFOcaCwHe2B9SOCWbDAN +JXpTjz1DmJO4ImyWPJpN1xsYKtm67Pefxmn0ax0uE2uuzq25h0xbTkqIQgJzyoE/ +DPkBFK1vDkMfAW11dQ0BXatEnW7Gtkc0lh2/PIbHWj4AzxYMyBf5Gy6HSVOftwjC +voQR2qr2xJBixsg+MIORKtmKHLfU + + + + + + + + + +MIIDEjCCAfqgAwIBAgIVAMECQ1tjghafm5OxWDh9hwZfxthWMA0GCSqGSIb3DQEB +CwUAMBYxFDASBgNVBAMMC3NhbWx0ZXN0LmlkMB4XDTE4MDgyNDIxMTQwOVoXDTM4 +MDgyNDIxMTQwOVowFjEUMBIGA1UEAwwLc2FtbHRlc3QuaWQwggEiMA0GCSqGSIb3 +DQEBAQUAA4IBDwAwggEKAoIBAQC0Z4QX1NFKs71ufbQwoQoW7qkNAJRIANGA4iM0 +ThYghul3pC+FwrGv37aTxWXfA1UG9njKbbDreiDAZKngCgyjxj0uJ4lArgkr4AOE +jj5zXA81uGHARfUBctvQcsZpBIxDOvUUImAl+3NqLgMGF2fktxMG7kX3GEVNc1kl +bN3dfYsaw5dUrw25DheL9np7G/+28GwHPvLb4aptOiONbCaVvh9UMHEA9F7c0zfF +/cL5fOpdVa54wTI0u12CsFKt78h6lEGG5jUs/qX9clZncJM7EFkN3imPPy+0HC8n +spXiH/MZW8o2cqWRkrw3MzBZW3Ojk5nQj40V6NUbjb7kfejzAgMBAAGjVzBVMB0G +A1UdDgQWBBQT6Y9J3Tw/hOGc8PNV7JEE4k2ZNTA0BgNVHREELTArggtzYW1sdGVz +dC5pZIYcaHR0cHM6Ly9zYW1sdGVzdC5pZC9zYW1sL2lkcDANBgkqhkiG9w0BAQsF +AAOCAQEASk3guKfTkVhEaIVvxEPNR2w3vWt3fwmwJCccW98XXLWgNbu3YaMb2RSn +7Th4p3h+mfyk2don6au7Uyzc1Jd39RNv80TG5iQoxfCgphy1FYmmdaSfO8wvDtHT +TNiLArAxOYtzfYbzb5QrNNH/gQEN8RJaEf/g/1GTw9x/103dSMK0RXtl+fRs2nbl +D1JJKSQ3AdhxK/weP3aUPtLxVVJ9wMOQOfcy02l+hHMb6uAjsPOpOVKqi3M8XmcU +ZOpx4swtgGdeoSpeRyrtMvRwdcciNBp9UZome44qZAYH1iqrpmmjsfI9pJItsgWu +3kXPjhSfj1AJGR1l9JGvJrHki1iHTA== + + + + + + + + + +MIIDEjCCAfqgAwIBAgIVAPVbodo8Su7/BaHXUHykx0Pi5CFaMA0GCSqGSIb3DQEB +CwUAMBYxFDASBgNVBAMMC3NhbWx0ZXN0LmlkMB4XDTE4MDgyNDIxMTQwOVoXDTM4 +MDgyNDIxMTQwOVowFjEUMBIGA1UEAwwLc2FtbHRlc3QuaWQwggEiMA0GCSqGSIb3 +DQEBAQUAA4IBDwAwggEKAoIBAQCQb+1a7uDdTTBBFfwOUun3IQ9nEuKM98SmJDWa +MwM877elswKUTIBVh5gB2RIXAPZt7J/KGqypmgw9UNXFnoslpeZbA9fcAqqu28Z4 +sSb2YSajV1ZgEYPUKvXwQEmLWN6aDhkn8HnEZNrmeXihTFdyr7wjsLj0JpQ+VUlc +4/J+hNuU7rGYZ1rKY8AA34qDVd4DiJ+DXW2PESfOu8lJSOteEaNtbmnvH8KlwkDs +1NvPTsI0W/m4SK0UdXo6LLaV8saIpJfnkVC/FwpBolBrRC/Em64UlBsRZm2T89ca +uzDee2yPUvbBd5kLErw+sC7i4xXa2rGmsQLYcBPhsRwnmBmlAgMBAAGjVzBVMB0G +A1UdDgQWBBRZ3exEu6rCwRe5C7f5QrPcAKRPUjA0BgNVHREELTArggtzYW1sdGVz +dC5pZIYcaHR0cHM6Ly9zYW1sdGVzdC5pZC9zYW1sL2lkcDANBgkqhkiG9w0BAQsF +AAOCAQEABZDFRNtcbvIRmblnZItoWCFhVUlq81ceSQddLYs8DqK340//hWNAbYdj +WcP85HhIZnrw6NGCO4bUipxZXhiqTA/A9d1BUll0vYB8qckYDEdPDduYCOYemKkD +dmnHMQWs9Y6zWiYuNKEJ9mf3+1N8knN/PK0TYVjVjXAf2CnOETDbLtlj6Nqb8La3 +sQkYmU+aUdopbjd5JFFwbZRaj6KiHXHtnIRgu8sUXNPrgipUgZUOVhP0C0N5OfE4 +JW8ZBrKgQC/6vJ2rSa9TlzI6JAa5Ww7gMXMP9M+cJUNQklcq+SBnTK8G+uBHgPKR +zBDsMIEzRtQZm4GIoHJae4zmnCekkQ== + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/resources/com/coveo/saml/samltest.id/sp.xml b/src/test/resources/com/coveo/saml/samltest.id/sp.xml new file mode 100644 index 0000000..8914613 --- /dev/null +++ b/src/test/resources/com/coveo/saml/samltest.id/sp.xml @@ -0,0 +1,128 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + JD Test SP + A free and basic SP for testing SAML deployments + https://samltest.id/saml/logo.png + + + + + + + + +MIIDujCCAqKgAwIBAgIJAJHfaVlpD2MLMA0GCSqGSIb3DQEBCwUAMHIxCzAJBgNV +BAYTAkZSMQowCAYDVQQIDAEtMQowCAYDVQQHDAEtMQowCAYDVQQKDAEtMQowCAYD +VQQLDAEtMRQwEgYDVQQDDAtzYW1sLWNsaWVudDEdMBsGCSqGSIb3DQEJARYObm9A +YWRkcmVzcy5jb20wHhcNMTgwOTIwMTMyOTM1WhcNMTkwOTIwMTMyOTM1WjByMQsw +CQYDVQQGEwJGUjEKMAgGA1UECAwBLTEKMAgGA1UEBwwBLTEKMAgGA1UECgwBLTEK +MAgGA1UECwwBLTEUMBIGA1UEAwwLc2FtbC1jbGllbnQxHTAbBgkqhkiG9w0BCQEW +Dm5vQGFkZHJlc3MuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +r4KWtZ++3+wF+P5vHBus58gPhM5z2eH3bCmYYMtz/sdssjzYs7gvDP4fCaQy2mma +ntMzJaYdLA08SON49yy7lcRK40E4MQ+DvksPS64bMwjRvLyN96ANKxJnoUejL6uI +86yGwH09ad/J/2GC0kmGgEctxZ60QHVqK5Bn4Yv/RoXC5jskbEIrbACJ8rwa7/8z +1rM8r1tsm7jy/8NqsHvFQ0IfB8N27zqdrC77XsaYZWHJzBbyaSC0AnAOOXXMOdoL +YpsaeXw5nZ3knwKY5PyEODuCv/5rOoJkpqdN5zX06yZ7uubQ1+o3cHyV/UErZmk7 +oPLvMezc/lwtUmHBvY0LMQIDAQABo1MwUTAdBgNVHQ4EFgQUoU2D/5UEJ1CiB8eH +OOb7p0OUSJkwHwYDVR0jBBgwFoAUoU2D/5UEJ1CiB8eHOOb7p0OUSJkwDwYDVR0T +AQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAE6c4YayrDUcXNcK20CUD0xSe +5Pf75gKBSf7s8C2EEk4kQ4W1yc5dG7pnCGhKNhOpeYWgTEHnFyb1311wDuhW2+a6 +uWLLkOr2R2qALPhPlMQVXKzA3smp8TydqFat1TTylrTo+lBmIWXBB9LczuqPpYKh +JOcJpDQW7J/asKpHnh+Jd7eMGs8PzI1sDoXEb3JuzMC1OQW/o750+IybAC3YH4jH +0uKlcKvIfC3puO99+Et3cZqgH0CMmsdy1KA8intCRcvIutm6upjZs4IFS+jC2mMn +EXZmiWfP5l4t8OPE0g5jdSuomDHAK63LNiCps4GVRXMtihRqN5j1+RGetfSUQA== + + + + + + + + + +MIIDujCCAqKgAwIBAgIJAJHfaVlpD2MLMA0GCSqGSIb3DQEBCwUAMHIxCzAJBgNV +BAYTAkZSMQowCAYDVQQIDAEtMQowCAYDVQQHDAEtMQowCAYDVQQKDAEtMQowCAYD +VQQLDAEtMRQwEgYDVQQDDAtzYW1sLWNsaWVudDEdMBsGCSqGSIb3DQEJARYObm9A +YWRkcmVzcy5jb20wHhcNMTgwOTIwMTMyOTM1WhcNMTkwOTIwMTMyOTM1WjByMQsw +CQYDVQQGEwJGUjEKMAgGA1UECAwBLTEKMAgGA1UEBwwBLTEKMAgGA1UECgwBLTEK +MAgGA1UECwwBLTEUMBIGA1UEAwwLc2FtbC1jbGllbnQxHTAbBgkqhkiG9w0BCQEW +Dm5vQGFkZHJlc3MuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +r4KWtZ++3+wF+P5vHBus58gPhM5z2eH3bCmYYMtz/sdssjzYs7gvDP4fCaQy2mma +ntMzJaYdLA08SON49yy7lcRK40E4MQ+DvksPS64bMwjRvLyN96ANKxJnoUejL6uI +86yGwH09ad/J/2GC0kmGgEctxZ60QHVqK5Bn4Yv/RoXC5jskbEIrbACJ8rwa7/8z +1rM8r1tsm7jy/8NqsHvFQ0IfB8N27zqdrC77XsaYZWHJzBbyaSC0AnAOOXXMOdoL +YpsaeXw5nZ3knwKY5PyEODuCv/5rOoJkpqdN5zX06yZ7uubQ1+o3cHyV/UErZmk7 +oPLvMezc/lwtUmHBvY0LMQIDAQABo1MwUTAdBgNVHQ4EFgQUoU2D/5UEJ1CiB8eH +OOb7p0OUSJkwHwYDVR0jBBgwFoAUoU2D/5UEJ1CiB8eHOOb7p0OUSJkwDwYDVR0T +AQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAE6c4YayrDUcXNcK20CUD0xSe +5Pf75gKBSf7s8C2EEk4kQ4W1yc5dG7pnCGhKNhOpeYWgTEHnFyb1311wDuhW2+a6 +uWLLkOr2R2qALPhPlMQVXKzA3smp8TydqFat1TTylrTo+lBmIWXBB9LczuqPpYKh +JOcJpDQW7J/asKpHnh+Jd7eMGs8PzI1sDoXEb3JuzMC1OQW/o750+IybAC3YH4jH +0uKlcKvIfC3puO99+Et3cZqgH0CMmsdy1KA8intCRcvIutm6upjZs4IFS+jC2mMn +EXZmiWfP5l4t8OPE0g5jdSuomDHAK63LNiCps4GVRXMtihRqN5j1+RGetfSUQA== + + + + + + + + + + + + + + + + + + + + + + + + + + + +