/* * Copyright 2016 Red Hat, Inc. and/or its affiliates * and other contributors as indicated by the @author tags. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.keycloak.saml; import org.jboss.logging.Logger; import org.keycloak.common.util.KeycloakUriBuilder; import org.keycloak.saml.common.constants.GeneralConstants; import org.keycloak.saml.common.constants.JBossSAMLConstants; import org.keycloak.saml.common.constants.JBossSAMLURIConstants; import org.keycloak.saml.common.exceptions.ConfigurationException; import org.keycloak.saml.common.exceptions.ProcessingException; import org.keycloak.saml.processing.api.saml.v2.sig.SAML2Signature; import org.keycloak.saml.processing.core.saml.v2.util.DocumentUtil; import org.keycloak.saml.processing.core.util.XMLEncryptionUtil; import org.keycloak.saml.processing.web.util.PostBindingUtil; import org.keycloak.saml.processing.web.util.RedirectBindingUtil; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; import javax.xml.crypto.dsig.CanonicalizationMethod; import javax.xml.namespace.QName; import java.io.IOException; import java.net.URI; import java.security.InvalidKeyException; import java.security.KeyPair; import java.security.PrivateKey; import java.security.PublicKey; import java.security.Signature; import java.security.SignatureException; import java.security.cert.X509Certificate; import static org.keycloak.common.util.HtmlUtils.escapeAttribute; import static org.keycloak.saml.common.util.StringUtil.isNotNull; /** * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @version $Revision: 1 $ */ public class BaseSAML2BindingBuilder<T extends BaseSAML2BindingBuilder> { protected static final Logger logger = Logger.getLogger(BaseSAML2BindingBuilder.class); protected String signingKeyName; protected KeyPair signingKeyPair; protected X509Certificate signingCertificate; protected boolean sign; protected boolean signAssertions; protected SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.RSA_SHA1; protected String relayState; protected int encryptionKeySize = 128; protected PublicKey encryptionPublicKey; protected String encryptionAlgorithm = "AES"; protected boolean encrypt; protected String canonicalizationMethodType = CanonicalizationMethod.EXCLUSIVE; public T canonicalizationMethod(String method) { this.canonicalizationMethodType = method; return (T)this; } public T signDocument() { this.sign = true; return (T)this; } public T signAssertions() { this.signAssertions = true; return (T)this; } public T signWith(String signingKeyName, KeyPair keyPair) { this.signingKeyName = signingKeyName; this.signingKeyPair = keyPair; return (T)this; } public T signWith(String signingKeyName, PrivateKey privateKey, PublicKey publicKey) { this.signingKeyName = signingKeyName; this.signingKeyPair = new KeyPair(publicKey, privateKey); return (T)this; } public T signWith(String signingKeyName, KeyPair keyPair, X509Certificate cert) { this.signingKeyName = signingKeyName; this.signingKeyPair = keyPair; this.signingCertificate = cert; return (T)this; } public T signWith(String signingKeyName, PrivateKey privateKey, PublicKey publicKey, X509Certificate cert) { this.signingKeyName = signingKeyName; this.signingKeyPair = new KeyPair(publicKey, privateKey); this.signingCertificate = cert; return (T)this; } public T signatureAlgorithm(SignatureAlgorithm alg) { this.signatureAlgorithm = alg; return (T)this; } public T encrypt(PublicKey publicKey) { encrypt = true; encryptionPublicKey = publicKey; return (T)this; } public T encryptionAlgorithm(String alg) { this.encryptionAlgorithm = alg; return (T)this; } public T encryptionKeySize(int size) { this.encryptionKeySize = size; return (T)this; } public T relayState(String relayState) { this.relayState = relayState; return (T)this; } public static class BasePostBindingBuilder { protected Document document; protected BaseSAML2BindingBuilder builder; public BasePostBindingBuilder(BaseSAML2BindingBuilder builder, Document document) throws ProcessingException { this.builder = builder; this.document = document; if (builder.signAssertions) { builder.signAssertion(document); } if (builder.encrypt) builder.encryptDocument(document); if (builder.sign) { builder.signDocument(document); } } public String encoded() throws ProcessingException, ConfigurationException, IOException { byte[] responseBytes = DocumentUtil.getDocumentAsString(document).getBytes(GeneralConstants.SAML_CHARSET); return PostBindingUtil.base64Encode(new String(responseBytes, GeneralConstants.SAML_CHARSET)); } public Document getDocument() { return document; } public String getHtmlResponse(String actionUrl) throws ProcessingException, ConfigurationException, IOException { String str = builder.buildHtmlPostResponse(document, actionUrl, false); return str; } public String getHtmlRequest(String actionUrl) throws ProcessingException, ConfigurationException, IOException { String str = builder.buildHtmlPostResponse(document, actionUrl, true); return str; } } public static class BaseRedirectBindingBuilder { protected Document document; protected BaseSAML2BindingBuilder builder; public BaseRedirectBindingBuilder(BaseSAML2BindingBuilder builder, Document document) throws ProcessingException { this.builder = builder; this.document = document; if (builder.encrypt) builder.encryptDocument(document); if (builder.signAssertions) { builder.signAssertion(document); } } public Document getDocument() { return document; } public URI generateURI(String redirectUri, boolean asRequest) throws ConfigurationException, ProcessingException, IOException { String samlParameterName = GeneralConstants.SAML_RESPONSE_KEY; if (asRequest) { samlParameterName = GeneralConstants.SAML_REQUEST_KEY; } return builder.generateRedirectUri(samlParameterName, redirectUri, document); } public URI requestURI(String actionUrl) throws ConfigurationException, ProcessingException, IOException { return builder.generateRedirectUri(GeneralConstants.SAML_REQUEST_KEY, actionUrl, document); } public URI responseURI(String actionUrl) throws ConfigurationException, ProcessingException, IOException { return builder.generateRedirectUri(GeneralConstants.SAML_RESPONSE_KEY, actionUrl, document); } } public BaseRedirectBindingBuilder redirectBinding(Document document) throws ProcessingException { return new BaseRedirectBindingBuilder(this, document); } public BasePostBindingBuilder postBinding(Document document) throws ProcessingException { return new BasePostBindingBuilder(this, document); } public String getSAMLNSPrefix(Document samlResponseDocument) { Node assertionElement = samlResponseDocument.getDocumentElement() .getElementsByTagNameNS(JBossSAMLURIConstants.ASSERTION_NSURI.get(), JBossSAMLConstants.ASSERTION.get()).item(0); if (assertionElement == null) { throw new IllegalStateException("Unable to find assertion in saml response document"); } return assertionElement.getPrefix(); } public void encryptDocument(Document samlDocument) throws ProcessingException { String samlNSPrefix = getSAMLNSPrefix(samlDocument); try { QName encryptedAssertionElementQName = new QName(JBossSAMLURIConstants.ASSERTION_NSURI.get(), JBossSAMLConstants.ENCRYPTED_ASSERTION.get(), samlNSPrefix); byte[] secret = RandomSecret.createRandomSecret(encryptionKeySize / 8); SecretKey secretKey = new SecretKeySpec(secret, encryptionAlgorithm); // encrypt the Assertion element and replace it with a EncryptedAssertion element. XMLEncryptionUtil.encryptElement(new QName(JBossSAMLURIConstants.ASSERTION_NSURI.get(), JBossSAMLConstants.ASSERTION.get(), samlNSPrefix), samlDocument, encryptionPublicKey, secretKey, encryptionKeySize, encryptedAssertionElementQName, true); } catch (Exception e) { throw new ProcessingException("failed to encrypt", e); } } public void signDocument(Document samlDocument) throws ProcessingException { String signatureMethod = signatureAlgorithm.getXmlSignatureMethod(); String signatureDigestMethod = signatureAlgorithm.getXmlSignatureDigestMethod(); SAML2Signature samlSignature = new SAML2Signature(); if (signatureMethod != null) { samlSignature.setSignatureMethod(signatureMethod); } if (signatureDigestMethod != null) { samlSignature.setDigestMethod(signatureDigestMethod); } Node nextSibling = samlSignature.getNextSiblingOfIssuer(samlDocument); samlSignature.setNextSibling(nextSibling); if (signingCertificate != null) { samlSignature.setX509Certificate(signingCertificate); } samlSignature.signSAMLDocument(samlDocument, signingKeyName, signingKeyPair, canonicalizationMethodType); } public void signAssertion(Document samlDocument) throws ProcessingException { Element originalAssertionElement = org.keycloak.saml.common.util.DocumentUtil.getChildElement(samlDocument.getDocumentElement(), new QName(JBossSAMLURIConstants.ASSERTION_NSURI.get(), JBossSAMLConstants.ASSERTION.get())); if (originalAssertionElement == null) return; Node clonedAssertionElement = originalAssertionElement.cloneNode(true); Document temporaryDocument; try { temporaryDocument = org.keycloak.saml.common.util.DocumentUtil.createDocument(); } catch (ConfigurationException e) { throw new ProcessingException(e); } temporaryDocument.adoptNode(clonedAssertionElement); temporaryDocument.appendChild(clonedAssertionElement); signDocument(temporaryDocument); samlDocument.adoptNode(clonedAssertionElement); Element parentNode = (Element) originalAssertionElement.getParentNode(); parentNode.replaceChild(clonedAssertionElement, originalAssertionElement); } public String buildHtmlPostResponse(Document responseDoc, String actionUrl, boolean asRequest) throws ProcessingException, ConfigurationException, IOException { byte[] responseBytes = org.keycloak.saml.common.util.DocumentUtil.getDocumentAsString(responseDoc).getBytes(GeneralConstants.SAML_CHARSET); String samlResponse = PostBindingUtil.base64Encode(new String(responseBytes, GeneralConstants.SAML_CHARSET)); return buildHtml(samlResponse, actionUrl, asRequest); } public String buildHtml(String samlResponse, String actionUrl, boolean asRequest) { StringBuilder builder = new StringBuilder(); String key = GeneralConstants.SAML_RESPONSE_KEY; if (asRequest) { key = GeneralConstants.SAML_REQUEST_KEY; } builder.append("<HTML>") .append("<HEAD>") .append("<TITLE>SAML HTTP Post Binding</TITLE>") .append("</HEAD>") .append("<BODY Onload=\"document.forms[0].submit()\">") .append("<FORM METHOD=\"POST\" ACTION=\"").append(actionUrl).append("\">") .append("<INPUT TYPE=\"HIDDEN\" NAME=\"").append(key).append("\"").append(" VALUE=\"").append(samlResponse).append("\"/>"); if (isNotNull(relayState)) { builder.append("<INPUT TYPE=\"HIDDEN\" NAME=\"RelayState\" " + "VALUE=\"").append(escapeAttribute(relayState)).append("\"/>"); } builder.append("<NOSCRIPT>") .append("<P>JavaScript is disabled. We strongly recommend to enable it. Click the button below to continue.</P>") .append("<INPUT TYPE=\"SUBMIT\" VALUE=\"CONTINUE\" />") .append("</NOSCRIPT>") .append("</FORM></BODY></HTML>"); return builder.toString(); } public String base64Encoded(Document document) throws ConfigurationException, ProcessingException, IOException { String documentAsString = DocumentUtil.getDocumentAsString(document); logger.debugv("saml document: {0}", documentAsString); byte[] responseBytes = documentAsString.getBytes(GeneralConstants.SAML_CHARSET); return RedirectBindingUtil.deflateBase64URLEncode(responseBytes); } public URI generateRedirectUri(String samlParameterName, String redirectUri, Document document) throws ConfigurationException, ProcessingException, IOException { KeycloakUriBuilder builder = KeycloakUriBuilder.fromUri(redirectUri) .replaceQuery(null) .queryParam(samlParameterName, base64Encoded(document)); if (relayState != null) { builder.queryParam("RelayState", relayState); } if (sign) { builder.queryParam(GeneralConstants.SAML_SIG_ALG_REQUEST_KEY, signatureAlgorithm.getXmlSignatureMethod()); URI uri = builder.build(); String rawQuery = uri.getRawQuery(); Signature signature = signatureAlgorithm.createSignature(); byte[] sig = new byte[0]; try { signature.initSign(signingKeyPair.getPrivate()); signature.update(rawQuery.getBytes(GeneralConstants.SAML_CHARSET)); sig = signature.sign(); } catch (InvalidKeyException | SignatureException e) { throw new ProcessingException(e); } String encodedSig = RedirectBindingUtil.base64URLEncode(sig); builder.queryParam(GeneralConstants.SAML_SIGNATURE_REQUEST_KEY, encodedSig); } return builder.build(); } }