Skip to content

Commit

Permalink
Merge pull request apereo#743 from Unicon/googleapps-saml-response
Browse files Browse the repository at this point in the history
GoogleService: SAML response refactoring
  • Loading branch information
SavvasMisaghMoayyed committed Dec 1, 2014
2 parents 955189c + dd7c01c commit 7c26449
Show file tree
Hide file tree
Showing 10 changed files with 1,206 additions and 713 deletions.
Original file line number Diff line number Diff line change
@@ -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";

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
/**
Expand All @@ -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 =
"<samlp:Response ID=\"<RESPONSE_ID>\" IssueInstant=\"<ISSUE_INSTANT>\" 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#\">"
+ "<samlp:Status>"
+ "<samlp:StatusCode Value=\"urn:oasis:names:tc:SAML:2.0:status:Success\" />"
+ "</samlp:Status>"
+ "<Assertion ID=\"<ASSERTION_ID>\""
+ " IssueInstant=\"2003-04-17T00:46:02Z\" Version=\"2.0\""
+ " xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\">"
+ "<Issuer>https://www.opensaml.org/IDP</Issuer>"
+ "<Subject>"
+ "<NameID Format=\"urn:oasis:names:tc:SAML:2.0:nameid-format:emailAddress\">"
+ "<USERNAME_STRING>"
+ "</NameID>"
+ "<SubjectConfirmation Method=\"urn:oasis:names:tc:SAML:2.0:cm:bearer\">"
+ "<SubjectConfirmationData Recipient=\"<ACS_URL>\" NotOnOrAfter=\"<NOT_ON_OR_AFTER>\" InResponseTo=\"<REQUEST_ID>\" />"
+ "</SubjectConfirmation>"
+ "</Subject>"
+ "<Conditions NotBefore=\"2003-04-17T00:46:02Z\""
+ " NotOnOrAfter=\"<NOT_ON_OR_AFTER>\">"
+ "<AudienceRestriction>"
+ "<Audience><ACS_URL></Audience>"
+ "</AudienceRestriction>"
+ "</Conditions>"
+ "<AuthnStatement AuthnInstant=\"<AUTHN_INSTANT>\">"
+ "<AuthnContext>"
+ "<AuthnContextClassRef>"
+ "urn:oasis:names:tc:SAML:2.0:ac:classes:Password"
+ "</AuthnContextClassRef>"
+ "</AuthnContext>"
+ "</AuthnStatement>"
+ "</Assertion></samlp:Response>";
private static final GoogleSaml20ObjectBuilder BUILDER = new GoogleSaml20ObjectBuilder();

private final String relayState;

Expand Down Expand Up @@ -154,6 +111,8 @@ protected GoogleAccountsService(final String id, final String originalUrl,
this.publicKey = publicKey;
this.requestId = requestId;
this.servicesManager = servicesManager;


}

/**
Expand All @@ -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;
Expand All @@ -194,10 +154,10 @@ public static GoogleAccountsService createServiceFrom(
public Response getResponse(final String ticketId) {
final Map<String, String> parameters = new HashMap<String, String>();
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);
}
Expand All @@ -214,150 +174,42 @@ public boolean isLoggedOutAlready() {

/**
* Construct SAML response.
*
* <a href="http://bit.ly/1uI8Ggu">See this reference for more info.</a>
* @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("<USERNAME_STRING>", userId);
samlResponse = samlResponse.replace("<RESPONSE_ID>", createID());
samlResponse = samlResponse.replace("<ISSUE_INSTANT>", currentDateTime);
samlResponse = samlResponse.replace("<AUTHN_INSTANT>", currentDateTime);
samlResponse = samlResponse.replaceAll("<NOT_ON_OR_AFTER>", currentDateTime);
samlResponse = samlResponse.replace("<ASSERTION_ID>", createID());
samlResponse = samlResponse.replaceAll("<ACS_URL>", getId());
return samlResponse.replace("<REQUEST_ID>", 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();
}

}
Loading

0 comments on commit 7c26449

Please sign in to comment.