/*
* Copyright (c) 2015, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
*
* WSO2 Inc. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.wso2.carbon.identity.application.authenticator.samlsso.util;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.xml.security.c14n.Canonicalizer;
import org.opensaml.common.impl.SAMLObjectContentReference;
import org.opensaml.saml2.core.RequestAbstractType;
import org.opensaml.xml.XMLObject;
import org.opensaml.xml.XMLObjectBuilder;
import org.opensaml.xml.io.Marshaller;
import org.opensaml.xml.io.MarshallerFactory;
import org.opensaml.xml.io.MarshallingException;
import org.opensaml.xml.security.SigningUtil;
import org.opensaml.xml.security.x509.X509Credential;
import org.opensaml.xml.signature.KeyInfo;
import org.opensaml.xml.signature.Signature;
import org.opensaml.xml.signature.SignatureException;
import org.opensaml.xml.signature.Signer;
import org.opensaml.xml.signature.X509Data;
import org.opensaml.xml.util.Base64;
import org.w3c.dom.Element;
import org.w3c.dom.bootstrap.DOMImplementationRegistry;
import org.w3c.dom.ls.DOMImplementationLS;
import org.w3c.dom.ls.LSOutput;
import org.w3c.dom.ls.LSSerializer;
import org.wso2.carbon.identity.application.authenticator.samlsso.exception.SAMLSSOException;
import org.wso2.carbon.identity.application.common.util.IdentityApplicationConstants;
import org.wso2.carbon.identity.application.common.util.IdentityApplicationManagementUtil;
import javax.xml.namespace.QName;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.security.cert.CertificateEncodingException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.zip.DataFormatException;
import java.util.zip.Inflater;
import java.util.zip.InflaterInputStream;
public class SSOUtils {
private SSOUtils() {
}
private static Log log = LogFactory.getLog(SSOUtils.class);
/**
* Generates a unique Id for Authentication Requests
*
* @return generated unique ID
*/
public static String createID() {
byte[] bytes = new byte[20]; // 160 bit
new Random().nextBytes(bytes);
char[] charMapping = {'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p'};
char[] chars = new char[40];
for (int i = 0; i < bytes.length; i++) {
int left = (bytes[i] >> 4) & 0x0f;
int right = bytes[i] & 0x0f;
chars[i * 2] = charMapping[left];
chars[i * 2 + 1] = charMapping[right];
}
return String.valueOf(chars);
}
/**
* Sign the SAML Request message
*
* @param request
* @param signatureAlgorithm
* @param digestAlgorithm
* @param includeCert
* @param credential
* @return
* @throws SAMLSSOException
*/
public static void setSignature(RequestAbstractType request, String signatureAlgorithm,
String digestAlgorithm, boolean includeCert, X509Credential x509Credential)
throws SAMLSSOException {
if (request == null) {
throw new IllegalArgumentException("Request cannot be null");
}
if (x509Credential == null) {
throw new IllegalArgumentException("X509Credential cannot be null");
}
if (x509Credential.getEntityCertificate() == null) {
throw new SAMLSSOException(
"IdP certificate is needed for AuthnRequest signing in POST binding");
}
//TODO use StringUtils.isBlank
if (StringUtils.isEmpty(signatureAlgorithm)) {
signatureAlgorithm = IdentityApplicationManagementUtil.getXMLSignatureAlgorithms().get(
IdentityApplicationConstants.XML.SignatureAlgorithm.RSA_SHA1);
}
if (StringUtils.isEmpty(digestAlgorithm)) {
digestAlgorithm = IdentityApplicationManagementUtil.getXMLDigestAlgorithms().get(
IdentityApplicationConstants.XML.DigestAlgorithm.SHA1);
}
Signature signature = (Signature) buildXMLObject(Signature.DEFAULT_ELEMENT_NAME);
signature.setSigningCredential(x509Credential);
signature.setSignatureAlgorithm(signatureAlgorithm);
signature.setCanonicalizationAlgorithm(Canonicalizer.ALGO_ID_C14N_EXCL_OMIT_COMMENTS);
if (includeCert) {
KeyInfo keyInfo = (KeyInfo) buildXMLObject(KeyInfo.DEFAULT_ELEMENT_NAME);
X509Data data = (X509Data) buildXMLObject(X509Data.DEFAULT_ELEMENT_NAME);
org.opensaml.xml.signature.X509Certificate cert = (org.opensaml.xml.signature.X509Certificate) buildXMLObject(org.opensaml.xml.signature.X509Certificate.DEFAULT_ELEMENT_NAME);
String value = null;
try {
value = org.apache.xml.security.utils.Base64.encode(x509Credential
.getEntityCertificate().getEncoded());
} catch (CertificateEncodingException e) {
throw new SAMLSSOException("Error getting the certificate to include in the signature", e);
}
cert.setValue(value);
data.getX509Certificates().add(cert);
keyInfo.getX509Datas().add(data);
signature.setKeyInfo(keyInfo);
}
request.setSignature(signature);
((SAMLObjectContentReference)signature.getContentReferences().get(0))
.setDigestAlgorithm(digestAlgorithm);
List<Signature> signatureList = new ArrayList<Signature>();
signatureList.add(signature);
// Marshall and Sign
MarshallerFactory marshallerFactory =
org.opensaml.xml.Configuration.getMarshallerFactory();
Marshaller marshaller = marshallerFactory.getMarshaller(request);
try {
marshaller.marshall(request);
} catch (MarshallingException e) {
throw new SAMLSSOException("Error while marshalling the SAML Request for signing", e);
}
org.apache.xml.security.Init.init();
try {
Signer.signObjects(signatureList);
} catch (SignatureException e) {
throw new SAMLSSOException("Error while signing the SAML Request", e);
}
}
public static void addSignatureToHTTPQueryString(StringBuilder httpQueryString,
String signatureAlgorithmURI, X509Credential credential) throws SAMLSSOException {
try {
httpQueryString.append("&SigAlg=");
httpQueryString
.append(URLEncoder.encode(signatureAlgorithmURI, "UTF-8").trim());
byte[] rawSignature = SigningUtil.signWithURI(credential, signatureAlgorithmURI,
httpQueryString.toString().getBytes("UTF-8"));
String base64Signature = Base64.encodeBytes(rawSignature, Base64.DONT_BREAK_LINES);
if (log.isDebugEnabled()) {
log.debug("Generated digital signature value (base64-encoded) {} " + base64Signature);
}
httpQueryString.append("&Signature=" + URLEncoder.encode(base64Signature, "UTF-8").trim());
} catch (org.opensaml.xml.security.SecurityException e) {
throw new SAMLSSOException("Unable to sign query string", e);
} catch (UnsupportedEncodingException e) {
// UTF-8 encoding is required to be supported by all JVMs
throw new SAMLSSOException("Error while adding signature to HTTP query string", e);
}
}
/**
* Builds SAML Elements
*
* @param objectQName
* @return
* @throws SAMLSSOException
*/
private static XMLObject buildXMLObject(QName objectQName) throws SAMLSSOException {
XMLObjectBuilder builder =
org.opensaml.xml.Configuration.getBuilderFactory()
.getBuilder(objectQName);
if (builder == null) {
throw new SAMLSSOException("Unable to retrieve builder for object QName " +
objectQName);
}
return builder.buildObject(objectQName.getNamespaceURI(), objectQName.getLocalPart(),
objectQName.getPrefix());
}
/**
* Decoding and deflating the encoded AuthReq
*
* @param encodedStr encoded AuthReq
* @return decoded AuthReq
*/
public static String decode(String encodedStr) throws SAMLSSOException {
try {
if(log.isDebugEnabled()){
log.debug(" >> encoded string in the SSOUtils/decode : " + encodedStr);
}
org.apache.commons.codec.binary.Base64 base64Decoder =
new org.apache.commons.codec.binary.Base64();
byte[] xmlBytes = encodedStr.getBytes("UTF-8");
byte[] base64DecodedByteArray = base64Decoder.decode(xmlBytes);
try {
//TODO if the request came in POST, inflating is wrong
Inflater inflater = new Inflater(true);
inflater.setInput(base64DecodedByteArray);
byte[] xmlMessageBytes = new byte[5000];
int resultLength = inflater.inflate(xmlMessageBytes);
if (!inflater.finished() ){
throw new RuntimeException("End of the compressed data stream has NOT been reached");
}
inflater.end();
String decodedString = new String(xmlMessageBytes, 0, resultLength, "UTF-8");
if (log.isDebugEnabled()) {
log.debug("Request message " + decodedString);
}
return decodedString;
} catch (DataFormatException e) {
ByteArrayInputStream bais = new ByteArrayInputStream(base64DecodedByteArray);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
InflaterInputStream iis = new InflaterInputStream(bais);
byte[] buf = new byte[1024];
int count = iis.read(buf);
while (count != -1) {
baos.write(buf, 0, count);
count = iis.read(buf);
}
iis.close();
String decodedStr = new String(baos.toByteArray(), Charset.forName("UTF-8"));
if (log.isDebugEnabled()) {
log.debug("Request message " + decodedStr);
}
return decodedStr;
}
} catch (IOException e) {
throw new SAMLSSOException("Error when decoding the SAML Request.", e);
}
}
public static String decodeForPost(String encodedStr)
throws SAMLSSOException {
try {
org.apache.commons.codec.binary.Base64 base64Decoder = new org.apache.commons.codec.binary.Base64();
byte[] xmlBytes = encodedStr.getBytes("UTF-8");
byte[] base64DecodedByteArray = base64Decoder.decode(xmlBytes);
String decodedString = new String(base64DecodedByteArray, "UTF-8");
if (log.isDebugEnabled()) {
log.debug("Request message " + decodedString);
}
return decodedString;
} catch (IOException e) {
throw new SAMLSSOException(
"Error when decoding the SAML Request.", e);
}
}
/**
* Serializing a SAML2 object into a String
*
* @param xmlObject object that needs to serialized.
* @return serialized object
* @throws SAMLSSOException
*/
public static String marshall(XMLObject xmlObject) throws SAMLSSOException {
try {
System.setProperty("javax.xml.parsers.DocumentBuilderFactory",
"org.apache.xerces.jaxp.DocumentBuilderFactoryImpl");
MarshallerFactory marshallerFactory = org.opensaml.xml.Configuration
.getMarshallerFactory();
Marshaller marshaller = marshallerFactory.getMarshaller(xmlObject);
Element element = marshaller.marshall(xmlObject);
ByteArrayOutputStream byteArrayOutputStrm = new ByteArrayOutputStream();
DOMImplementationRegistry registry = DOMImplementationRegistry.newInstance();
DOMImplementationLS impl = (DOMImplementationLS) registry.getDOMImplementation("LS");
LSSerializer writer = impl.createLSSerializer();
LSOutput output = impl.createLSOutput();
output.setByteStream(byteArrayOutputStrm);
writer.write(element, output);
return byteArrayOutputStrm.toString();
} catch (Exception e) {
log.error("Error Serializing the SAML Response");
throw new SAMLSSOException("Error Serializing the SAML Response", e);
}
}
/**
* Encoding the response
*
* @param xmlString String to be encoded
* @return encoded String
*/
public static String encode(String xmlString) {
String encodedRequestMessage = Base64.encodeBytes(xmlString.getBytes(), Base64.DONT_BREAK_LINES);
return encodedRequestMessage.trim();
}
public static boolean isAuthnRequestSigned(Map<String, String> properties) {
if (properties != null) {
String prop = properties.get(IdentityApplicationConstants.Authenticator.SAML2SSO.IS_AUTHN_REQ_SIGNED);
if (prop != null) {
return Boolean.parseBoolean(prop);
}
}
return false;
}
public static boolean isLogoutEnabled(Map<String, String> properties) {
if (properties != null) {
String prop = properties.get(IdentityApplicationConstants.Authenticator.SAML2SSO.IS_LOGOUT_ENABLED);
if (prop != null) {
return Boolean.parseBoolean(prop);
}
}
return false;
}
public static boolean isLogoutRequestSigned(Map<String, String> properties) {
if (properties != null) {
String prop = properties.get(IdentityApplicationConstants.Authenticator.SAML2SSO.IS_LOGOUT_REQ_SIGNED);
if (prop != null) {
return Boolean.parseBoolean(prop);
}
}
return false;
}
public static boolean isAuthnResponseSigned(Map<String, String> properties) {
if (properties != null) {
String prop = properties.get(IdentityApplicationConstants.Authenticator.SAML2SSO.IS_AUTHN_RESP_SIGNED);
if (prop != null) {
return Boolean.parseBoolean(prop);
}
}
return false;
}
public static boolean isAssertionSigningEnabled(Map<String, String> properties) {
if (properties != null) {
String prop = properties.get(IdentityApplicationConstants.Authenticator.SAML2SSO.IS_ENABLE_ASSERTION_SIGNING);
if (prop != null) {
return Boolean.parseBoolean(prop);
}
}
return false;
}
public static boolean isAssertionEncryptionEnabled(Map<String, String> properties) {
if (properties != null) {
String prop = properties.get(IdentityApplicationConstants.Authenticator.SAML2SSO.IS_ENABLE_ASSERTION_ENCRYPTION);
if (prop != null) {
return Boolean.parseBoolean(prop);
}
}
return false;
}
public static Map<String, String> getQueryMap(String query) {
String[] params = query.split("&");
Map<String, String> map = new HashMap<String, String>();
for (String param : params) {
String[] paramSplitArr = param.split("=");
String name = paramSplitArr[0];
String value = "";
if (paramSplitArr.length > 1) {
value = paramSplitArr[1];
}
map.put(name, value);
}
return map;
}
}